diff --git a/apps/client/public/tools/axe1_64_2.png b/apps/client/public/tools/axe1_64_2.png deleted file mode 100644 index ee4f308d..00000000 Binary files a/apps/client/public/tools/axe1_64_2.png and /dev/null differ diff --git a/src/lib/assets/game/audio/chop-1.wav b/src/lib/assets/game/audio/chop-1.wav new file mode 100644 index 00000000..d76c89bf Binary files /dev/null and b/src/lib/assets/game/audio/chop-1.wav differ diff --git a/src/lib/assets/game/audio/fire-1.wav b/src/lib/assets/game/audio/fire-1.wav new file mode 100644 index 00000000..839ec9d4 Binary files /dev/null and b/src/lib/assets/game/audio/fire-1.wav differ diff --git a/src/lib/assets/game/audio/forest-1.mp3 b/src/lib/assets/game/audio/forest-1.mp3 new file mode 100644 index 00000000..385dfe97 Binary files /dev/null and b/src/lib/assets/game/audio/forest-1.mp3 differ diff --git a/src/lib/assets/game/audio/hand-punch-1.wav b/src/lib/assets/game/audio/hand-punch-1.wav new file mode 100644 index 00000000..9afaa4b7 Binary files /dev/null and b/src/lib/assets/game/audio/hand-punch-1.wav differ diff --git a/src/lib/assets/game/audio/marching-with-horns-1.wav b/src/lib/assets/game/audio/marching-with-horns-1.wav new file mode 100644 index 00000000..25879073 Binary files /dev/null and b/src/lib/assets/game/audio/marching-with-horns-1.wav differ diff --git a/src/lib/assets/game/audio/mine-1.wav b/src/lib/assets/game/audio/mine-1.wav new file mode 100644 index 00000000..cf0102b0 Binary files /dev/null and b/src/lib/assets/game/audio/mine-1.wav differ diff --git a/src/lib/assets/game/audio/wagon-1.wav b/src/lib/assets/game/audio/wagon-1.wav new file mode 100644 index 00000000..f0025026 Binary files /dev/null and b/src/lib/assets/game/audio/wagon-1.wav differ diff --git a/src/lib/assets/game/audio/yeah-1.wav b/src/lib/assets/game/audio/yeah-1.wav new file mode 100644 index 00000000..bd1aa563 Binary files /dev/null and b/src/lib/assets/game/audio/yeah-1.wav differ diff --git a/src/lib/assets/game/images/animation/campfire/1.png b/src/lib/assets/game/images/animation/campfire/1.png new file mode 100644 index 00000000..4f9adad5 Binary files /dev/null and b/src/lib/assets/game/images/animation/campfire/1.png differ diff --git a/src/lib/assets/game/images/animation/campfire/2.png b/src/lib/assets/game/images/animation/campfire/2.png new file mode 100644 index 00000000..63c1b1ac Binary files /dev/null and b/src/lib/assets/game/images/animation/campfire/2.png differ diff --git a/src/lib/assets/game/images/animation/campfire/3.png b/src/lib/assets/game/images/animation/campfire/3.png new file mode 100644 index 00000000..62e9d9e3 Binary files /dev/null and b/src/lib/assets/game/images/animation/campfire/3.png differ diff --git a/src/lib/assets/game/images/animation/campfire/4.png b/src/lib/assets/game/images/animation/campfire/4.png new file mode 100644 index 00000000..5e663302 Binary files /dev/null and b/src/lib/assets/game/images/animation/campfire/4.png differ diff --git a/src/lib/assets/game/images/animation/campfire/5.png b/src/lib/assets/game/images/animation/campfire/5.png new file mode 100644 index 00000000..7a25cbe3 Binary files /dev/null and b/src/lib/assets/game/images/animation/campfire/5.png differ diff --git a/src/lib/assets/game/images/animation/campfire/particle-1.png b/src/lib/assets/game/images/animation/campfire/particle-1.png new file mode 100644 index 00000000..9b4c0659 Binary files /dev/null and b/src/lib/assets/game/images/animation/campfire/particle-1.png differ diff --git a/src/lib/assets/game/images/animation/campfire/particle-2.png b/src/lib/assets/game/images/animation/campfire/particle-2.png new file mode 100644 index 00000000..7c26c095 Binary files /dev/null and b/src/lib/assets/game/images/animation/campfire/particle-2.png differ diff --git a/src/lib/assets/game/images/animation/campfire/particle-3.png b/src/lib/assets/game/images/animation/campfire/particle-3.png new file mode 100644 index 00000000..945ff039 Binary files /dev/null and b/src/lib/assets/game/images/animation/campfire/particle-3.png differ diff --git a/src/lib/assets/game/images/animation/hero-moving-right/1.png b/src/lib/assets/game/images/animation/hero-moving-right/1.png new file mode 100644 index 00000000..17ecc617 Binary files /dev/null and b/src/lib/assets/game/images/animation/hero-moving-right/1.png differ diff --git a/src/lib/assets/game/images/animation/hero-moving-right/2.png b/src/lib/assets/game/images/animation/hero-moving-right/2.png new file mode 100644 index 00000000..584152d6 Binary files /dev/null and b/src/lib/assets/game/images/animation/hero-moving-right/2.png differ diff --git a/src/lib/assets/game/images/animation/hero-moving-right/3.png b/src/lib/assets/game/images/animation/hero-moving-right/3.png new file mode 100644 index 00000000..e60369f0 Binary files /dev/null and b/src/lib/assets/game/images/animation/hero-moving-right/3.png differ diff --git a/src/lib/assets/game/images/animation/hero-moving-right/4.png b/src/lib/assets/game/images/animation/hero-moving-right/4.png new file mode 100644 index 00000000..85ac447e Binary files /dev/null and b/src/lib/assets/game/images/animation/hero-moving-right/4.png differ diff --git a/src/lib/assets/game/images/animation/hero-moving-right/5.png b/src/lib/assets/game/images/animation/hero-moving-right/5.png new file mode 100644 index 00000000..df47794d Binary files /dev/null and b/src/lib/assets/game/images/animation/hero-moving-right/5.png differ diff --git a/src/lib/assets/game/images/animation/hero-moving-right/6.png b/src/lib/assets/game/images/animation/hero-moving-right/6.png new file mode 100644 index 00000000..f81f8a21 Binary files /dev/null and b/src/lib/assets/game/images/animation/hero-moving-right/6.png differ diff --git a/src/lib/assets/game/images/background/1.png b/src/lib/assets/game/images/background/1.png new file mode 100644 index 00000000..5bc7fcab Binary files /dev/null and b/src/lib/assets/game/images/background/1.png differ diff --git a/src/lib/assets/game/images/background/2.png b/src/lib/assets/game/images/background/2.png new file mode 100644 index 00000000..f267bea9 Binary files /dev/null and b/src/lib/assets/game/images/background/2.png differ diff --git a/src/lib/assets/game/images/background/3.png b/src/lib/assets/game/images/background/3.png new file mode 100644 index 00000000..68de7b9e Binary files /dev/null and b/src/lib/assets/game/images/background/3.png differ diff --git a/src/lib/assets/game/images/background/4.png b/src/lib/assets/game/images/background/4.png new file mode 100644 index 00000000..b76292fb Binary files /dev/null and b/src/lib/assets/game/images/background/4.png differ diff --git a/src/lib/assets/game/images/background/5.png b/src/lib/assets/game/images/background/5.png new file mode 100644 index 00000000..263edc45 Binary files /dev/null and b/src/lib/assets/game/images/background/5.png differ diff --git a/src/lib/assets/game/images/background/generated-1.png b/src/lib/assets/game/images/background/generated-1.png new file mode 100644 index 00000000..48d02b63 Binary files /dev/null and b/src/lib/assets/game/images/background/generated-1.png differ diff --git a/src/lib/assets/game/images/background/paper-background-1.jpg b/src/lib/assets/game/images/background/paper-background-1.jpg new file mode 100644 index 00000000..b8a8b9f9 Binary files /dev/null and b/src/lib/assets/game/images/background/paper-background-1.jpg differ diff --git a/src/lib/assets/game/images/buildings/camp-fire-1.png b/src/lib/assets/game/images/buildings/camp-fire-1.png new file mode 100644 index 00000000..f9eaf5d2 Binary files /dev/null and b/src/lib/assets/game/images/buildings/camp-fire-1.png differ diff --git a/src/lib/assets/game/images/buildings/camp-fire-2.png b/src/lib/assets/game/images/buildings/camp-fire-2.png new file mode 100644 index 00000000..dcb01b1d Binary files /dev/null and b/src/lib/assets/game/images/buildings/camp-fire-2.png differ diff --git a/src/lib/assets/game/images/buildings/construction-1.png b/src/lib/assets/game/images/buildings/construction-1.png new file mode 100644 index 00000000..518f7b33 Binary files /dev/null and b/src/lib/assets/game/images/buildings/construction-1.png differ diff --git a/src/lib/assets/game/images/buildings/store.png b/src/lib/assets/game/images/buildings/store.png new file mode 100644 index 00000000..adb7d533 Binary files /dev/null and b/src/lib/assets/game/images/buildings/store.png differ diff --git a/src/lib/assets/game/images/buildings/wagon-stop-1.png b/src/lib/assets/game/images/buildings/wagon-stop-1.png new file mode 100644 index 00000000..5c1cd1ef Binary files /dev/null and b/src/lib/assets/game/images/buildings/wagon-stop-1.png differ diff --git a/src/lib/assets/game/images/buildings/warehouse-1.png b/src/lib/assets/game/images/buildings/warehouse-1.png new file mode 100644 index 00000000..b5b49dc6 Binary files /dev/null and b/src/lib/assets/game/images/buildings/warehouse-1.png differ diff --git a/src/lib/assets/game/images/creatures/rabbit1_left_64.png b/src/lib/assets/game/images/creatures/rabbit1_left_64.png new file mode 100644 index 00000000..d292acfc Binary files /dev/null and b/src/lib/assets/game/images/creatures/rabbit1_left_64.png differ diff --git a/src/lib/assets/game/images/creatures/rabbit1_right_64.png b/src/lib/assets/game/images/creatures/rabbit1_right_64.png new file mode 100644 index 00000000..c63619e8 Binary files /dev/null and b/src/lib/assets/game/images/creatures/rabbit1_right_64.png differ diff --git a/src/lib/assets/game/images/creatures/wolf1_left_64.png b/src/lib/assets/game/images/creatures/wolf1_left_64.png new file mode 100644 index 00000000..caa634b8 Binary files /dev/null and b/src/lib/assets/game/images/creatures/wolf1_left_64.png differ diff --git a/src/lib/assets/game/images/creatures/wolf1_right_64.png b/src/lib/assets/game/images/creatures/wolf1_right_64.png new file mode 100644 index 00000000..90a06356 Binary files /dev/null and b/src/lib/assets/game/images/creatures/wolf1_right_64.png differ diff --git a/src/lib/assets/game/images/icons/coin-1.png b/src/lib/assets/game/images/icons/coin-1.png new file mode 100644 index 00000000..a4307355 Binary files /dev/null and b/src/lib/assets/game/images/icons/coin-1.png differ diff --git a/src/lib/assets/game/images/icons/fuel-1.png b/src/lib/assets/game/images/icons/fuel-1.png new file mode 100644 index 00000000..d76efc75 Binary files /dev/null and b/src/lib/assets/game/images/icons/fuel-1.png differ diff --git a/src/lib/assets/game/images/icons/items/coin.png b/src/lib/assets/game/images/icons/items/coin.png new file mode 100644 index 00000000..a4307355 Binary files /dev/null and b/src/lib/assets/game/images/icons/items/coin.png differ diff --git a/src/lib/assets/game/images/icons/items/stone.png b/src/lib/assets/game/images/icons/items/stone.png new file mode 100644 index 00000000..4f5b32f1 Binary files /dev/null and b/src/lib/assets/game/images/icons/items/stone.png differ diff --git a/src/lib/assets/game/images/icons/items/wood.png b/src/lib/assets/game/images/icons/items/wood.png new file mode 100644 index 00000000..2a9e0218 Binary files /dev/null and b/src/lib/assets/game/images/icons/items/wood.png differ diff --git a/src/lib/assets/game/images/icons/speed-1.png b/src/lib/assets/game/images/icons/speed-1.png new file mode 100644 index 00000000..e770975e Binary files /dev/null and b/src/lib/assets/game/images/icons/speed-1.png differ diff --git a/src/lib/assets/game/images/objects/flag-1.png b/src/lib/assets/game/images/objects/flag-1.png new file mode 100644 index 00000000..44138cf2 Binary files /dev/null and b/src/lib/assets/game/images/objects/flag-1.png differ diff --git a/src/lib/assets/game/images/objects/flag-2.png b/src/lib/assets/game/images/objects/flag-2.png new file mode 100644 index 00000000..a4652afc Binary files /dev/null and b/src/lib/assets/game/images/objects/flag-2.png differ diff --git a/src/lib/assets/game/images/objects/stone-1.png b/src/lib/assets/game/images/objects/stone-1.png new file mode 100644 index 00000000..02c43535 Binary files /dev/null and b/src/lib/assets/game/images/objects/stone-1.png differ diff --git a/src/lib/assets/game/images/objects/tree/1/blue.png b/src/lib/assets/game/images/objects/tree/1/blue.png new file mode 100644 index 00000000..c2b2457b Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/1/blue.png differ diff --git a/src/lib/assets/game/images/objects/tree/1/green.png b/src/lib/assets/game/images/objects/tree/1/green.png new file mode 100644 index 00000000..c4b29589 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/1/green.png differ diff --git a/src/lib/assets/game/images/objects/tree/1/stone.png b/src/lib/assets/game/images/objects/tree/1/stone.png new file mode 100644 index 00000000..b154a4b6 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/1/stone.png differ diff --git a/src/lib/assets/game/images/objects/tree/1/teal.png b/src/lib/assets/game/images/objects/tree/1/teal.png new file mode 100644 index 00000000..4f2b05a4 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/1/teal.png differ diff --git a/src/lib/assets/game/images/objects/tree/1/toxic.png b/src/lib/assets/game/images/objects/tree/1/toxic.png new file mode 100644 index 00000000..256bf429 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/1/toxic.png differ diff --git a/src/lib/assets/game/images/objects/tree/1/violet.png b/src/lib/assets/game/images/objects/tree/1/violet.png new file mode 100644 index 00000000..ed7dc9d9 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/1/violet.png differ diff --git a/src/lib/assets/game/images/objects/tree/2/blue.png b/src/lib/assets/game/images/objects/tree/2/blue.png new file mode 100644 index 00000000..33f59b0f Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/2/blue.png differ diff --git a/src/lib/assets/game/images/objects/tree/2/green.png b/src/lib/assets/game/images/objects/tree/2/green.png new file mode 100644 index 00000000..ddb6a160 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/2/green.png differ diff --git a/src/lib/assets/game/images/objects/tree/2/stone.png b/src/lib/assets/game/images/objects/tree/2/stone.png new file mode 100644 index 00000000..13108060 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/2/stone.png differ diff --git a/src/lib/assets/game/images/objects/tree/2/teal.png b/src/lib/assets/game/images/objects/tree/2/teal.png new file mode 100644 index 00000000..2b2fe0f3 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/2/teal.png differ diff --git a/src/lib/assets/game/images/objects/tree/2/toxic.png b/src/lib/assets/game/images/objects/tree/2/toxic.png new file mode 100644 index 00000000..f126962b Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/2/toxic.png differ diff --git a/src/lib/assets/game/images/objects/tree/2/violet.png b/src/lib/assets/game/images/objects/tree/2/violet.png new file mode 100644 index 00000000..6abf2853 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/2/violet.png differ diff --git a/src/lib/assets/game/images/objects/tree/3/blue.png b/src/lib/assets/game/images/objects/tree/3/blue.png new file mode 100644 index 00000000..071e9a16 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/3/blue.png differ diff --git a/src/lib/assets/game/images/objects/tree/3/green.png b/src/lib/assets/game/images/objects/tree/3/green.png new file mode 100644 index 00000000..0c408459 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/3/green.png differ diff --git a/src/lib/assets/game/images/objects/tree/3/stone.png b/src/lib/assets/game/images/objects/tree/3/stone.png new file mode 100644 index 00000000..387bf222 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/3/stone.png differ diff --git a/src/lib/assets/game/images/objects/tree/3/teal.png b/src/lib/assets/game/images/objects/tree/3/teal.png new file mode 100644 index 00000000..ff426c58 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/3/teal.png differ diff --git a/src/lib/assets/game/images/objects/tree/3/toxic.png b/src/lib/assets/game/images/objects/tree/3/toxic.png new file mode 100644 index 00000000..c408f5c7 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/3/toxic.png differ diff --git a/src/lib/assets/game/images/objects/tree/3/violet.png b/src/lib/assets/game/images/objects/tree/3/violet.png new file mode 100644 index 00000000..baa1adc7 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/3/violet.png differ diff --git a/src/lib/assets/game/images/objects/tree/4/blue.png b/src/lib/assets/game/images/objects/tree/4/blue.png new file mode 100644 index 00000000..d1a6485c Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/4/blue.png differ diff --git a/src/lib/assets/game/images/objects/tree/4/green.png b/src/lib/assets/game/images/objects/tree/4/green.png new file mode 100644 index 00000000..b2729d46 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/4/green.png differ diff --git a/src/lib/assets/game/images/objects/tree/4/stone.png b/src/lib/assets/game/images/objects/tree/4/stone.png new file mode 100644 index 00000000..93f2737a Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/4/stone.png differ diff --git a/src/lib/assets/game/images/objects/tree/4/teal.png b/src/lib/assets/game/images/objects/tree/4/teal.png new file mode 100644 index 00000000..942d8a81 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/4/teal.png differ diff --git a/src/lib/assets/game/images/objects/tree/4/toxic.png b/src/lib/assets/game/images/objects/tree/4/toxic.png new file mode 100644 index 00000000..aebc3238 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/4/toxic.png differ diff --git a/src/lib/assets/game/images/objects/tree/4/violet.png b/src/lib/assets/game/images/objects/tree/4/violet.png new file mode 100644 index 00000000..2323eb4f Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/4/violet.png differ diff --git a/src/lib/assets/game/images/objects/tree/5/blue.png b/src/lib/assets/game/images/objects/tree/5/blue.png new file mode 100644 index 00000000..a7f9b491 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/5/blue.png differ diff --git a/src/lib/assets/game/images/objects/tree/5/green.png b/src/lib/assets/game/images/objects/tree/5/green.png new file mode 100644 index 00000000..6103efdb Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/5/green.png differ diff --git a/src/lib/assets/game/images/objects/tree/5/stone.png b/src/lib/assets/game/images/objects/tree/5/stone.png new file mode 100644 index 00000000..68928561 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/5/stone.png differ diff --git a/src/lib/assets/game/images/objects/tree/5/teal.png b/src/lib/assets/game/images/objects/tree/5/teal.png new file mode 100644 index 00000000..f42ec6b0 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/5/teal.png differ diff --git a/src/lib/assets/game/images/objects/tree/5/toxic.png b/src/lib/assets/game/images/objects/tree/5/toxic.png new file mode 100644 index 00000000..7695da7f Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/5/toxic.png differ diff --git a/src/lib/assets/game/images/objects/tree/5/violet.png b/src/lib/assets/game/images/objects/tree/5/violet.png new file mode 100644 index 00000000..71d85a34 Binary files /dev/null and b/src/lib/assets/game/images/objects/tree/5/violet.png differ diff --git a/src/lib/assets/game/images/tools/axe1_64.png b/src/lib/assets/game/images/tools/axe1_64.png new file mode 100644 index 00000000..78414aa6 Binary files /dev/null and b/src/lib/assets/game/images/tools/axe1_64.png differ diff --git a/src/lib/assets/game/images/tools/pickaxe1_64.png b/src/lib/assets/game/images/tools/pickaxe1_64.png new file mode 100644 index 00000000..7ec61c22 Binary files /dev/null and b/src/lib/assets/game/images/tools/pickaxe1_64.png differ diff --git a/src/lib/assets/game/images/unit/hairstyle/hair-classic-right.png b/src/lib/assets/game/images/unit/hairstyle/hair-classic-right.png new file mode 100644 index 00000000..a8614bd1 Binary files /dev/null and b/src/lib/assets/game/images/unit/hairstyle/hair-classic-right.png differ diff --git a/src/lib/assets/game/images/unit/hairstyle/hair-coal-long-right.png b/src/lib/assets/game/images/unit/hairstyle/hair-coal-long-right.png new file mode 100644 index 00000000..b944061e Binary files /dev/null and b/src/lib/assets/game/images/unit/hairstyle/hair-coal-long-right.png differ diff --git a/src/lib/assets/game/images/unit/hairstyle/orange-with-beard.png b/src/lib/assets/game/images/unit/hairstyle/orange-with-beard.png new file mode 100644 index 00000000..c0d8c48d Binary files /dev/null and b/src/lib/assets/game/images/unit/hairstyle/orange-with-beard.png differ diff --git a/src/lib/assets/game/images/unit/head/head-1-right.png b/src/lib/assets/game/images/unit/head/head-1-right.png new file mode 100644 index 00000000..e6fdeffd Binary files /dev/null and b/src/lib/assets/game/images/unit/head/head-1-right.png differ diff --git a/src/lib/assets/game/images/unit/top/black-top-right.png b/src/lib/assets/game/images/unit/top/black-top-right.png new file mode 100644 index 00000000..57f74ecd Binary files /dev/null and b/src/lib/assets/game/images/unit/top/black-top-right.png differ diff --git a/src/lib/assets/game/images/unit/top/blue-top-right.png b/src/lib/assets/game/images/unit/top/blue-top-right.png new file mode 100644 index 00000000..db0ef6c8 Binary files /dev/null and b/src/lib/assets/game/images/unit/top/blue-top-right.png differ diff --git a/src/lib/assets/game/images/unit/top/dark-silver-top-right.png b/src/lib/assets/game/images/unit/top/dark-silver-top-right.png new file mode 100644 index 00000000..0d2c0feb Binary files /dev/null and b/src/lib/assets/game/images/unit/top/dark-silver-top-right.png differ diff --git a/src/lib/assets/game/images/unit/top/green-top-right.png b/src/lib/assets/game/images/unit/top/green-top-right.png new file mode 100644 index 00000000..bdfc5df6 Binary files /dev/null and b/src/lib/assets/game/images/unit/top/green-top-right.png differ diff --git a/src/lib/assets/game/images/unit/top/violet-top-right.png b/src/lib/assets/game/images/unit/top/violet-top-right.png new file mode 100644 index 00000000..c9950acc Binary files /dev/null and b/src/lib/assets/game/images/unit/top/violet-top-right.png differ diff --git a/src/lib/assets/game/images/wagon/chest-1.png b/src/lib/assets/game/images/wagon/chest-1.png new file mode 100644 index 00000000..df644b98 Binary files /dev/null and b/src/lib/assets/game/images/wagon/chest-1.png differ diff --git a/src/lib/assets/game/images/wagon/clouds/1.png b/src/lib/assets/game/images/wagon/clouds/1.png new file mode 100644 index 00000000..c42cd97d Binary files /dev/null and b/src/lib/assets/game/images/wagon/clouds/1.png differ diff --git a/src/lib/assets/game/images/wagon/clouds/2.png b/src/lib/assets/game/images/wagon/clouds/2.png new file mode 100644 index 00000000..e19d36a1 Binary files /dev/null and b/src/lib/assets/game/images/wagon/clouds/2.png differ diff --git a/src/lib/assets/game/images/wagon/clouds/3.png b/src/lib/assets/game/images/wagon/clouds/3.png new file mode 100644 index 00000000..bc13e610 Binary files /dev/null and b/src/lib/assets/game/images/wagon/clouds/3.png differ diff --git a/src/lib/assets/game/images/wagon/clouds/4.png b/src/lib/assets/game/images/wagon/clouds/4.png new file mode 100644 index 00000000..25ad0282 Binary files /dev/null and b/src/lib/assets/game/images/wagon/clouds/4.png differ diff --git a/src/lib/assets/game/images/wagon/engine-1.png b/src/lib/assets/game/images/wagon/engine-1.png new file mode 100644 index 00000000..27a0b79f Binary files /dev/null and b/src/lib/assets/game/images/wagon/engine-1.png differ diff --git a/src/lib/assets/game/images/wagon/fuel-1.png b/src/lib/assets/game/images/wagon/fuel-1.png new file mode 100644 index 00000000..4c8d0e24 Binary files /dev/null and b/src/lib/assets/game/images/wagon/fuel-1.png differ diff --git a/src/lib/assets/game/images/wagon/fuel-2.png b/src/lib/assets/game/images/wagon/fuel-2.png new file mode 100644 index 00000000..dbd146b5 Binary files /dev/null and b/src/lib/assets/game/images/wagon/fuel-2.png differ diff --git a/src/lib/assets/game/images/wagon/fuel-container.png b/src/lib/assets/game/images/wagon/fuel-container.png new file mode 100644 index 00000000..1e2cfd0a Binary files /dev/null and b/src/lib/assets/game/images/wagon/fuel-container.png differ diff --git a/src/lib/assets/game/images/wagon/wagon-1.png b/src/lib/assets/game/images/wagon/wagon-1.png new file mode 100644 index 00000000..c59632c9 Binary files /dev/null and b/src/lib/assets/game/images/wagon/wagon-1.png differ diff --git a/src/lib/assets/game/images/wagon/wagon-2.png b/src/lib/assets/game/images/wagon/wagon-2.png new file mode 100644 index 00000000..7b3c6833 Binary files /dev/null and b/src/lib/assets/game/images/wagon/wagon-2.png differ diff --git a/src/lib/assets/game/images/wagon/wheel-1.png b/src/lib/assets/game/images/wagon/wheel-1.png new file mode 100644 index 00000000..a57a0496 Binary files /dev/null and b/src/lib/assets/game/images/wagon/wheel-1.png differ diff --git a/src/lib/assets/game/images/water/1.png b/src/lib/assets/game/images/water/1.png new file mode 100644 index 00000000..155b6ad0 Binary files /dev/null and b/src/lib/assets/game/images/water/1.png differ diff --git a/src/lib/assets/game/images/water/2.png b/src/lib/assets/game/images/water/2.png new file mode 100644 index 00000000..4eba0919 Binary files /dev/null and b/src/lib/assets/game/images/water/2.png differ diff --git a/src/lib/assets/game/images/water/3.png b/src/lib/assets/game/images/water/3.png new file mode 100644 index 00000000..b44cb514 Binary files /dev/null and b/src/lib/assets/game/images/water/3.png differ diff --git a/src/lib/assets/game/images/water/4.png b/src/lib/assets/game/images/water/4.png new file mode 100644 index 00000000..727115b1 Binary files /dev/null and b/src/lib/assets/game/images/water/4.png differ diff --git a/src/lib/date.ts b/src/lib/date.ts new file mode 100644 index 00000000..1170c50e --- /dev/null +++ b/src/lib/date.ts @@ -0,0 +1,14 @@ +export function getDatePlusMinutes(minutes: number) { + const milliseconds = minutes * 60 * 1000 + return new Date(new Date().getTime() + milliseconds) +} + +export function getDateMinusMinutes(minutes: number) { + const milliseconds = minutes * 60 * 1000 + return new Date(new Date().getTime() - milliseconds) +} + +export function getDatePlusSeconds(seconds: number) { + const milliseconds = seconds * 1000 + return new Date(new Date().getTime() + milliseconds) +} diff --git a/src/lib/game/actions/action.ts b/src/lib/game/actions/action.ts new file mode 100644 index 00000000..261ec351 --- /dev/null +++ b/src/lib/game/actions/action.ts @@ -0,0 +1,22 @@ +import type { IGameAction, IGameActionResponse, } from "$lib/game/types" +import type { Player } from "../objects/units" + +interface IActionOptions { + command: IGameAction["command"] + commandDescription: IGameAction["commandDescription"] +} + +export class Action implements IGameAction { + public command: string + public commandDescription: string + + public live!: ( + player: Player, + params: string[], + ) => Promise + + constructor({ command, commandDescription }: IActionOptions) { + this.command = command + this.commandDescription = commandDescription + } +} diff --git a/src/lib/game/actions/donateWoodToVillageAction.ts b/src/lib/game/actions/donateWoodToVillageAction.ts new file mode 100644 index 00000000..917b75b6 --- /dev/null +++ b/src/lib/game/actions/donateWoodToVillageAction.ts @@ -0,0 +1,43 @@ +import { Village } from "../chunks" +import type { Warehouse } from "../objects/buildings/warehouse" +import type { Player } from "../objects/units" +import type { GameScene } from "../scenes/gameScene" +import { Action } from "./action" +import { ANSWER } from "$lib/game/services/actionService"; + +interface IDonateWoodToVillageActionOptions { + scene: GameScene +} + +export class DonateWoodToVillageAction extends Action { + private scene: GameScene + + constructor({ scene }: IDonateWoodToVillageActionOptions) { + super({ command: "donate", commandDescription: "!donate [quantity]" }) + + this.scene = scene + this.live = this.initLive + } + + async initLive(player: Player, params: string[]) { + const amount = this.scene.actionService.getAmountFromChatCommand(params[0]) + if (!amount) { + return ANSWER.WRONG_AMOUNT_ERROR + } + + let warehouse: Warehouse | undefined + if (this.scene.chunkNow instanceof Village) { + warehouse = this.scene.chunkNow.getWarehouse() + } + + const isSuccess = await player.inventory.reduceOrDestroyItem("WOOD", amount) + if (!isSuccess) { + return ANSWER.NOT_ENOUGH_WOOD_ERROR + } + + await warehouse?.inventory.addOrCreateItem("WOOD", amount) + await player.addReputation(amount) + + return ANSWER.DONATE_WOOD_OK + } +} diff --git a/src/lib/game/actions/plantTreeAction.ts b/src/lib/game/actions/plantTreeAction.ts new file mode 100644 index 00000000..b00e6585 --- /dev/null +++ b/src/lib/game/actions/plantTreeAction.ts @@ -0,0 +1,50 @@ +import { Village } from "../chunks" +import type { Player } from "../objects/units" +import type { GameScene } from "../scenes" +import { PlantNewTreeScript } from "../scripts/plantNewTreeScript" +import { Action } from "./action" +import { ANSWER } from "$lib/game/services/actionService"; + +interface IPlantTreeActionOptions { + scene: GameScene +} + +export class PlantTreeAction extends Action { + private scene: GameScene + + constructor({ scene }: IPlantTreeActionOptions) { + super({ command: "plant", commandDescription: "!plant" }) + + this.scene = scene + this.live = this.initLive + } + + async initLive(player: Player) { + if (player.script && !player.script.isInterruptible) { + return ANSWER.BUSY_ERROR + } + + if (this.scene.chunkNow instanceof Village) { + const target = this.scene.chunkNow.checkIfNeedToPlantTree() + if (!target) { + return ANSWER.NO_SPACE_AVAILABLE_ERROR + } + + const plantNewTreeFunc = () => { + if (this.scene.chunkNow instanceof Village) { + this.scene.chunkNow.plantNewTree(target) + } + } + + player.script = new PlantNewTreeScript({ + object: player, + target, + plantNewTreeFunc, + }) + + return ANSWER.OK + } + + return ANSWER.ERROR + } +} diff --git a/src/lib/game/actions/voteAction.ts b/src/lib/game/actions/voteAction.ts new file mode 100644 index 00000000..bec1a52c --- /dev/null +++ b/src/lib/game/actions/voteAction.ts @@ -0,0 +1,34 @@ +import type { Poll } from "../common" +import type { Player } from "../objects/units" +import { Action } from "./action" +import { ANSWER } from "$lib/game/services/actionService"; + +interface IVoteActionOptions { + poll: Poll +} + +export class VoteAction extends Action { + private poll: Poll + private readonly id: string + + constructor({ poll }: IVoteActionOptions) { + super({ command: "go", commandDescription: "!go" }) + + this.id = poll.generatePollId() + + this.command = `go ${this.id}` + this.commandDescription = `!go ${this.id}` + + this.poll = poll + this.live = this.initLive + } + + async initLive(player: Player) { + const isSuccess = this.poll.vote(player) + if (!isSuccess) { + return ANSWER.ALREADY_VOTED_ERROR + } + + return ANSWER.VOTED_OK + } +} diff --git a/src/lib/game/chunks/forest.ts b/src/lib/game/chunks/forest.ts new file mode 100644 index 00000000..e3df1007 --- /dev/null +++ b/src/lib/game/chunks/forest.ts @@ -0,0 +1,71 @@ +import { type IGameChunkTheme, type IGameForestChunk, } from "$lib/game/types" +import { Stone, Tree } from "../objects" +import type { GameScene } from "../scenes/gameScene.ts" +import { GameChunk } from "./gameChunk" +import { getRandomInRange } from "$lib/random"; + +interface IForestOptions { + center: IGameForestChunk["center"] + width: number + height: number + theme: IGameChunkTheme + scene: GameScene +} + +export class Forest extends GameChunk implements IGameForestChunk { + constructor({ width, height, center, theme, scene }: IForestOptions) { + super({ + title: "Grand Wood", + type: "FOREST", + width, + height, + center, + theme, + scene, + }) + + const treesToPrepare = Math.round( + (this.area.area.endX - this.area.area.startX) / 10, + ) + this.initTrees(treesToPrepare) + this.initStones(3) + } + + live() { + super.live() + + for (const obj of this.objects) { + void obj.live() + } + } + + initTrees(count: number) { + for (let i = 0; i < count; i++) { + const point = this.getRandomPoint() + const size = getRandomInRange(75, 90) + const tree = new Tree({ + scene: this.scene, + x: point.x, + y: point.y, + size, + resource: 1, + health: 20, + variant: this.area.theme, + }) + this.objects.push(tree) + } + } + + initStones(count: number) { + for (let i = 0; i < count; i++) { + const point = this.getRandomPoint() + const stone = new Stone({ + scene: this.scene, + x: point.x, + y: point.y, + resource: 1, + }) + this.objects.push(stone) + } + } +} diff --git a/src/lib/game/chunks/gameChunk.ts b/src/lib/game/chunks/gameChunk.ts new file mode 100644 index 00000000..7eed3ad3 --- /dev/null +++ b/src/lib/game/chunks/gameChunk.ts @@ -0,0 +1,120 @@ +import { createId } from "@paralleldrive/cuid2" +import { type IGameChunk, type IGameChunkTheme, } from "$lib/game/types" +import { Area, type GameObject, Tree } from "../objects" +import type { GameScene } from "../scenes/gameScene.ts" +import { getRandomInRange } from "$lib/random"; + +interface IGameChunkOptions { + center: IGameChunk["center"] + title: IGameChunk["title"] + type: IGameChunk["type"] + theme: IGameChunkTheme + width: number + height: number + scene: GameScene +} + +export class GameChunk implements IGameChunk { + public id: string + public title: string + public type: IGameChunk["type"] + public center!: IGameChunk["center"] + public area!: Area + + public scene: GameScene + public objects: GameObject[] = [] + + constructor({ + title, + type, + theme, + width, + height, + center, + scene, + }: IGameChunkOptions) { + this.id = createId() + this.center = center + this.title = title + this.type = type + + this.scene = scene + + this.initArea({ width, height, theme }) + } + + public live() { + } + + private initArea({ + width, + height, + theme, + }: { + width: number + height: number + theme: IGameChunkTheme + }) { + const halfWidth = Math.round(width / 2) + const halfHeight = Math.round(height / 2) + + const area = { + startX: this.center.x - halfWidth, + endX: this.center.x + halfWidth, + startY: this.center.y - halfHeight, + endY: this.center.y + halfHeight, + } + + this.area = new Area({ scene: this.scene, theme, area }) + } + + public getRandomPoint() { + return { + x: getRandomInRange(this.area.area.startX, this.area.area.endX), + y: getRandomInRange(this.area.area.startY, this.area.area.endY), + } + } + + public getRandomOutPoint() { + const height = this.area.area.endY - this.area.area.startY + const offsetFromTop = Math.round(height / 4) + + return { + x: this.area.area.endX, + y: getRandomInRange( + this.area.area.startY + offsetFromTop, + this.area.area.endY, + ), + } + } + + public checkIfPointIsInArea(point: { x: number; y: number }): boolean { + if (point.x >= this.area.area.startX && point.x <= this.area.area.endX) { + if (point.y >= this.area.area.startY && point.y <= this.area.area.endY) { + return true + } + } + + return false + } + + removeObject(object: GameObject) { + const index = this.objects.indexOf(object) + this.objects.splice(index, 1) + } + + getAvailableTree(): Tree | undefined { + const trees = this.objects.filter( + (obj) => + obj instanceof Tree && + obj.state !== "DESTROYED" && + !obj.isReserved && + obj.isReadyToChop, + ) + if (!trees || !trees.length) { + return undefined + } + + return trees[Math.floor(Math.random() * trees.length)] as Tree + } +} diff --git a/src/lib/game/chunks/index.ts b/src/lib/game/chunks/index.ts new file mode 100644 index 00000000..f795be06 --- /dev/null +++ b/src/lib/game/chunks/index.ts @@ -0,0 +1,4 @@ +export { Forest } from "./forest" +export { GameChunk } from "./gameChunk" +export { Village } from "./village" +export { LakeChunk } from "./lake" diff --git a/src/lib/game/chunks/lake.ts b/src/lib/game/chunks/lake.ts new file mode 100644 index 00000000..bcf19487 --- /dev/null +++ b/src/lib/game/chunks/lake.ts @@ -0,0 +1,86 @@ +import { type IGameChunkTheme, type IGameLakeChunk, } from "$lib/game/types" +import { Lake, Stone, Tree } from "../objects" +import { GameChunk } from "./gameChunk" +import type { GameScene } from "../scenes/gameScene.ts"; +import { getRandomInRange } from "$lib/random"; + +interface ILakeOptions { + scene: GameScene + center: IGameLakeChunk["center"] + width: number + height: number + theme: IGameChunkTheme +} + +export class LakeChunk extends GameChunk implements IGameLakeChunk { + constructor({ scene, width, height, center, theme }: ILakeOptions) { + super({ + scene, + width, + height, + center, + theme, + title: "Lake with a Secret", + type: "LAKE", + }) + + const treesToPrepare = Math.round( + (this.area.area.endX - this.area.area.startX) / 30, + ) + this.initTrees(treesToPrepare) + this.initStones(3) + this.initLake() + } + + live() { + super.live() + + for (const obj of this.objects) { + void obj.live() + } + } + + initLake() { + const lake = new Lake({ + scene: this.scene, + x: this.center.x - 100, + y: this.center.y + 400 + }) + const lake2 = new Lake({ + scene: this.scene, + x: this.center.x - 600, + y: this.center.y + 500 + }) + this.objects.push(lake, lake2) + } + + initTrees(count: number) { + for (let i = 0; i < count; i++) { + const point = this.getRandomPoint() + const size = getRandomInRange(75, 90) + const tree = new Tree({ + scene: this.scene, + x: point.x, + y: point.y, + size, + resource: 1, + health: 20, + variant: this.area.theme, + }) + this.objects.push(tree) + } + } + + initStones(count: number) { + for (let i = 0; i < count; i++) { + const point = this.getRandomPoint() + const stone = new Stone({ + scene: this.scene, + x: point.x, + y: point.y, + resource: 1 + }) + this.objects.push(stone) + } + } +} diff --git a/src/lib/game/chunks/village.ts b/src/lib/game/chunks/village.ts new file mode 100644 index 00000000..7aae0bca --- /dev/null +++ b/src/lib/game/chunks/village.ts @@ -0,0 +1,418 @@ +import { + type IGameChunkTheme, + type IGameObjectFlag, + type IGameVillageChunk, +} from "$lib/game/types" +import { Flag, Stone, Tree } from "../objects" +import { Campfire } from "../objects/buildings/campfire" +import { ConstructionArea } from "../objects/buildings/constructionArea" +import { Store } from "../objects/buildings/store" +import { WagonStop } from "../objects/buildings/wagonStop" +import { Warehouse } from "../objects/buildings/warehouse" +import { Courier, Farmer } from "../objects/units" +import type { GameScene } from "../scenes/gameScene" +import { BuildScript } from "../scripts/buildScript" +import { ChopTreeScript } from "../scripts/chopTreeScript" +import { MoveToTargetScript } from "../scripts/moveToTargetScript" +import { + PlaceItemInWarehouseScript +} from "../scripts/placeItemInWarehouseScript" +import { PlantNewTreeScript } from "../scripts/plantNewTreeScript" +import { GameChunk } from "./gameChunk" +import { getRandomInRange } from "$lib/random"; + +interface IVillageOptions { + scene: GameScene + width: number + height: number + center: IGameVillageChunk["center"] + theme: IGameChunkTheme +} + +export class Village extends GameChunk implements IGameVillageChunk { + constructor({ width, height, center, theme, scene }: IVillageOptions) { + super({ title: "", type: "VILLAGE", theme, width, height, center, scene }) + + this.title = this.getRandomTitle() + + this.initFlags("RESOURCE", 80) + //this.initFlags("MOVEMENT", 30) + this.initTrees(20) + this.initStones(5) + + this.initCourier(1) + this.initFarmer(1) + this.initBuildings() + } + + live() { + super.live() + + for (const object of this.objects) { + if (object instanceof Farmer && !object.script) { + this.addTaskToFarmer(object) + continue + } + + if (object instanceof Courier && !object.script) { + this.addTaskToCourier(object) + } + } + } + + addTaskToCourier(object: Courier) { + const random = getRandomInRange(1, 500) + if (random !== 1) { + return + } + + // Need to build Store + const warehouse = this.getWarehouse() + const store = this.getStore() + const wood = warehouse?.getItemByType("WOOD") + if (wood?.amount && wood.amount >= 25 && !store) { + // Let's build! + const target = this.getConstructionArea() + if (!target) { + return + } + + const buildFunc = (): boolean => { + warehouse?.inventory.reduceOrDestroyItem("WOOD", 25) + this.buildStore() + + return true + } + object.script = new BuildScript({ + object, + target, + buildFunc, + }) + + return + } + + // If unit have smth in inventory + const item = object.inventory.checkIfAlreadyHaveItem("WOOD") + if (item) { + const target = this.getWarehouse() + if (!target) { + return + } + + const placeItemFunc = () => { + if (object.target instanceof Warehouse) { + void object.target.inventory.addOrCreateItem(item.type, item.amount) + void object.inventory.destroyItem(item.id) + } + } + object.script = new PlaceItemInWarehouseScript({ + object, + target, + placeItemFunc, + }) + + return + } + + // If there is an available tree + const availableTree = this.getAvailableTree() + if (availableTree) { + const chopTreeFunc = (): boolean => { + object.chopTree() + if (!object.target || object.target.state === "DESTROYED") { + object.state = "IDLE" + if (object.target instanceof Tree) { + void object.inventory.addOrCreateItem( + "WOOD", + object.target?.resource, + ) + } + return true + } + return false + } + + object.script = new ChopTreeScript({ + object, + target: availableTree, + chopTreeFunc, + }) + + return + } + + const target = this.getRandomMovementFlagInVillage() + if (!target) { + return + } + object.script = new MoveToTargetScript({ + object, + target, + }) + } + + addTaskToFarmer(object: Farmer) { + const target = this.checkIfNeedToPlantTree() + if (target) { + const plantNewTreeFunc = () => { + this.plantNewTree(target) + } + + object.script = new PlantNewTreeScript({ + object, + target, + plantNewTreeFunc, + }) + return + } + + // No Trees needed? + const random = getRandomInRange(1, 300) + if (random <= 1) { + const target = this.getRandomMovementFlagInVillage() + if (!target) { + return + } + object.script = new MoveToTargetScript({ + object, + target, + }) + return + } + } + + initFlag(type: IGameObjectFlag["type"]) { + const randomPoint = this.getRandomPoint() + this.objects.push( + new Flag({ + scene: this.scene, + type, + x: randomPoint.x, + y: randomPoint.y, + }), + ) + } + + initFlags(type: IGameObjectFlag["type"], count: number) { + for (let i = 0; i < count; i++) { + this.initFlag(type) + } + } + + initTrees(count: number) { + for (let i = 0; i < count; i++) { + const flag = this.getRandomEmptyResourceFlagInVillage() + if (flag) { + const size = getRandomInRange(65, 85) + const tree = new Tree({ + scene: this.scene, + x: flag.x, + y: flag.y, + size, + resource: 1, + health: 20, + variant: this.area.theme, + }) + flag.target = tree + + this.objects.push(tree) + } + } + } + + initStones(count: number) { + for (let i = 0; i < count; i++) { + const flag = this.getRandomEmptyResourceFlagInVillage() + if (flag) { + const stone = new Stone({ + scene: this.scene, + x: flag.x, + y: flag.y, + resource: 1, + }) + flag.target = stone + this.objects.push(stone) + } + } + } + + initCourier(count = 1) { + for (let i = 0; i < count; i++) { + const randomPoint = this.getRandomPoint() + const courier = new Courier({ + scene: this.scene, + x: randomPoint.x, + y: randomPoint.y, + }) + + this.objects.push(courier) + } + } + + initFarmer(count = 1) { + for (let i = 0; i < count; i++) { + const randomPoint = this.getRandomPoint() + const farmer = new Farmer({ + scene: this.scene, + x: randomPoint.x, + y: randomPoint.y, + }) + + this.objects.push(farmer) + } + } + + initBuildings() { + const campfire = new Campfire({ + scene: this.scene, + x: this.center.x, + y: this.center.y, + }) + + const warehouse = new Warehouse({ + scene: this.scene, + x: this.center.x + 270, + y: this.center.y - 150, + }) + + const wagonStop = new WagonStop({ + scene: this.scene, + x: this.center.x - 780, + y: this.center.y + 280, + }) + + const constructionArea = new ConstructionArea({ + scene: this.scene, + x: this.center.x + 600, + y: this.center.y + 250, + }) + + this.objects.push(campfire, warehouse, wagonStop, constructionArea) + } + + buildStore() { + const constructionArea = this.getConstructionArea() + if (!constructionArea) { + return + } + + constructionArea.state = "DESTROYED" + + this.removeObject(constructionArea) + this.objects.push( + new Store({ + scene: this.scene, + x: constructionArea.x, + y: constructionArea.y, + }), + ) + } + + getWarehouse() { + return this.objects.find((b) => b instanceof Warehouse) as + | Warehouse + | undefined + } + + getStore() { + return this.objects.find((b) => b instanceof Store) as Store | undefined + } + + getConstructionArea() { + return this.objects.find((b) => b instanceof ConstructionArea) as + | ConstructionArea + | undefined + } + + public getWagonStopPoint() { + for (const object of this.objects) { + if (object instanceof WagonStop) { + return { x: object.x, y: object.y } + } + } + return { x: 500, y: 500 } + } + + getRandomEmptyResourceFlagInVillage() { + const flags = this.objects.filter( + (f) => + f instanceof Flag && + f.type === "RESOURCE" && + !f.target && + !f.isReserved, + ) + return flags.length > 0 + ? (flags[Math.floor(Math.random() * flags.length)] as Flag) + : undefined + } + + getResourceFlagInVillageAmount() { + return this.objects.filter( + (f) => f instanceof Flag && f.type === "RESOURCE", + ).length + } + + getRandomMovementFlagInVillage() { + const flags = this.objects.filter( + (f) => f instanceof Flag && f.type === "MOVEMENT", + ) + return flags.length > 0 + ? flags[Math.floor(Math.random() * flags.length)] + : undefined + } + + getRandomTitle() { + const titles = [ + "Windy Peak", + "Green Grove", + "Oak Coast", + "Forest Harbor", + "Elven Forest", + "Stone Outpost", + "Watermelon Paradise", + "Magic Valley", + "Royal Haven", + "Phantom Cliff", + ] + return titles[Math.floor(Math.random() * titles.length)] + } + + checkIfNeedToPlantTree() { + const treesNow = this.objects.filter( + (t) => t instanceof Tree && t.state !== "DESTROYED", + ) + if (treesNow.length < 40) { + return this.getRandomEmptyResourceFlagInVillage() + } + } + + plantNewTree(flag: Flag) { + const tree = new Tree({ + scene: this.scene, + x: flag.x, + y: flag.y, + resource: 1, + size: 12, + health: 20, + variant: this.area.theme, + }) + + flag.target = tree + flag.isReserved = false + this.objects.push(tree) + } + + getTreesAmount() { + return this.objects.filter( + (obj) => obj instanceof Tree && obj.state !== "DESTROYED", + ).length + } + + checkIfThereAreNotEnoughTrees() { + const max = this.getResourceFlagInVillageAmount() + const now = this.getTreesAmount() + + return now < max / 3 + } +} diff --git a/src/lib/game/common/event.ts b/src/lib/game/common/event.ts new file mode 100644 index 00000000..54502015 --- /dev/null +++ b/src/lib/game/common/event.ts @@ -0,0 +1,69 @@ +import { createId } from "@paralleldrive/cuid2" +import { type GameSceneType, type IGameEvent, } from "$lib/game/types" +import { getDatePlusSeconds } from "$lib/date"; + +interface IEventOptions { + title: IGameEvent["title"] + description: IGameEvent["description"] + type: IGameEvent["type"] + secondsToEnd: number + scene?: GameSceneType + poll: IGameEvent["poll"] + quest: IGameEvent["quest"] + offers: IGameEvent["offers"] +} + +export class Event implements IGameEvent { + public id: string + public title: IGameEvent["title"] + public description: IGameEvent["description"] + public type: IGameEvent["type"] + public status: IGameEvent["status"] + public scene?: GameSceneType + public endsAt!: Date + public deletesAt!: Date + public poll?: IGameEvent["poll"] + public quest?: IGameEvent["quest"] + public offers?: IGameEvent["offers"] + + constructor({ + title, + description, + type, + secondsToEnd, + scene, + poll, + quest, + offers, + }: IEventOptions) { + this.id = createId() + this.title = title + this.description = description + this.type = type + this.scene = scene + this.poll = poll + this.quest = quest + this.offers = offers + this.status = "STARTED" + + this.setEndsAtPlusSeconds(secondsToEnd) + + //sendMessage(type) + } + + public checkStatus() { + if (this.endsAt.getTime() <= new Date().getTime()) { + this.status = "STOPPED" + } + if (this.deletesAt.getTime() <= new Date().getTime()) { + this.status = "STOPPED" + } + + return this.status + } + + public setEndsAtPlusSeconds(seconds: number) { + this.endsAt = getDatePlusSeconds(seconds) + this.deletesAt = getDatePlusSeconds(seconds + 30) + } +} diff --git a/src/lib/game/common/generators/unitName.ts b/src/lib/game/common/generators/unitName.ts new file mode 100644 index 00000000..b7b6ea68 --- /dev/null +++ b/src/lib/game/common/generators/unitName.ts @@ -0,0 +1,29 @@ +export const generateUnitUserName = (): string => { + const maleNames = [ + "Valto Bizu", + "Zapris Hlel", + "Sinkmire Winglace", + "Hakir Elisor", + "Mapitu Uldan", + "Aetes Shangueiros", + "Nemesion Balgran", + "Garffon Lisalor", + "Golash Aena", + "Alistair al Pair", + "Sasgix Eranal", + "Petrosque Quinal", + "Laegon Umeran", + "Hersperon Oderle", + "Callister Grafft", + "Zangard Kaalin", + "Xernes Adafin", + "Xanus Elhora", + "Alistair Chapira", + "Salvestro Elmaris", + "Elusidor Riecto", + "Usir Lierin", + "Golash Yosalr", + ] + + return maleNames[Math.floor(Math.random() * maleNames.length)] +} diff --git a/src/lib/game/common/generators/unitTop.ts b/src/lib/game/common/generators/unitTop.ts new file mode 100644 index 00000000..eae16907 --- /dev/null +++ b/src/lib/game/common/generators/unitTop.ts @@ -0,0 +1,15 @@ +import type { IGameObjectUnit } from "$lib/game/types" + +export const generateUnitTop = (): Partial< + IGameObjectUnit["visual"]["top"] +> => { + const availableTopsForUnits: IGameObjectUnit["visual"]["top"][] = [ + "GREEN_SHIRT", + "BLUE_SHIRT", + "DARK_SILVER_SHIRT", + ] + + return availableTopsForUnits[ + Math.floor(Math.random() * availableTopsForUnits.length) + ] +} diff --git a/src/lib/game/common/group.ts b/src/lib/game/common/group.ts new file mode 100644 index 00000000..c6b447b5 --- /dev/null +++ b/src/lib/game/common/group.ts @@ -0,0 +1,54 @@ +import { createId } from "@paralleldrive/cuid2" +import type { IGameGroup, IGameObjectPlayer, } from "$lib/game/types" +import type { Player } from "../objects/units" + +export class Group implements IGameGroup { + id: string + players: IGameObjectPlayer[] = [] + + constructor() { + this.id = createId() + } + + public getGroup(): IGameGroup { + return { + id: this.id, + players: this.players.map((p) => { + return { + ...p, + script: undefined, + live: undefined, + } + }), + } + } + + join(player: Player): boolean { + const check = this.findPlayer(player.id) + if (check) { + return false + } + + this.players.push(player) + return true + } + + remove(player: IGameObjectPlayer): boolean { + const check = this.findPlayer(player.id) + if (!check) { + return false + } + + const index = this.players.indexOf(player) + this.players.splice(index, 1) + return true + } + + findPlayer(id: string) { + return this.players.find((p) => p.id === id) + } + + disband() { + this.players = [] + } +} diff --git a/src/lib/game/common/index.ts b/src/lib/game/common/index.ts new file mode 100644 index 00000000..07ad0570 --- /dev/null +++ b/src/lib/game/common/index.ts @@ -0,0 +1,6 @@ +export { Inventory } from "./inventory" +export { Skill } from "./skill" +export { Event } from "./event" +export { Group } from "./group" +export { Route } from "./route" +export { Poll } from "./poll" diff --git a/src/lib/game/common/inventory.ts b/src/lib/game/common/inventory.ts new file mode 100644 index 00000000..6aa7b178 --- /dev/null +++ b/src/lib/game/common/inventory.ts @@ -0,0 +1,181 @@ +import { createId } from "@paralleldrive/cuid2" +import type { + IGameInventory, + IGameInventoryItem, + ItemType, +} from "$lib/game/types" + +interface IInventoryOptions { + objectId: string + id: string + saveInDb: boolean +} + +export class Inventory implements IGameInventory { + public id: string + public objectId: string + public items: IGameInventoryItem[] = [] + public saveInDb: boolean + + constructor({ id, objectId, saveInDb }: IInventoryOptions) { + this.id = id + this.objectId = objectId + this.saveInDb = saveInDb + } + + public async init() { + await this.updateFromDB() + } + + public async destroyItem(id: string) { + const itemIndex = this.items.findIndex((i) => i.id === id) + if (itemIndex < 0) { + return + } + + this.items.splice(itemIndex, 1) + + if (this.saveInDb) { + await this.destroyItemInDB(id) + } + } + + public async reduceOrDestroyItem(type: ItemType, amount: number) { + const item = this.checkIfAlreadyHaveItem(type) + if (!item) { + return false + } + + if (amount > item.amount) { + return false + } + + if (amount === item.amount) { + await this.destroyItem(item.id) + return true + } + + if (this.saveInDb) { + await this.decrementAmountOfItemInDB(item.id, amount) + } + + item.amount -= amount + return true + } + + public async addOrCreateItem(type: ItemType, amount: number) { + if (this.saveInDb) { + return this.addOrCreateItemInDB(type, amount) + } + + const item = this.checkIfAlreadyHaveItem(type) + if (!item) { + this.createItem(type, amount) + return + } + + item.amount += amount + } + + async addOrCreateItemInDB(type: ItemType, amount: number) { + const item = await this.checkIfAlreadyHaveItemInDB(this.id, type) + if (!item) { + await this.createItemInDB(this.id, type, amount) + await this.updateFromDB() + return + } + + await this.incrementAmountOfItemInDB(item.id, amount) + await this.updateFromDB() + } + + async destroyItemInDB(id: string) { + // await db.inventoryItem.delete({ + // where: { id }, + // }) + await this.updateFromDB() + } + + public tryGetItemInDB(type: ItemType) { + return this.checkIfAlreadyHaveItemInDB(this.id, type) + } + + async checkAndBreakItem(item: IGameInventoryItem, decrement: number) { + if (item.durability <= decrement) { + await this.destroyItemInDB(item.id) + await this.updateFromDB() + return + } + + item.durability -= decrement + // await db.inventoryItem.update({ + // where: { id: item.id }, + // data: { + // durability: { decrement }, + // }, + // }) + } + + createItemInDB(inventoryId: string, type: ItemType, amount: number) { + // return db.inventoryItem.create({ + // data: { + // id: createId(), + // type, + // inventoryId, + // amount, + // }, + // }) + } + + incrementAmountOfItemInDB(id: string, amount: number) { + // return db.inventoryItem.update({ + // where: { id }, + // data: { + // amount: { + // increment: amount, + // }, + // }, + // }) + } + + decrementAmountOfItemInDB(id: string, amount: number) { + // return db.inventoryItem.update({ + // where: { id }, + // data: { + // amount: { + // decrement: amount, + // }, + // }, + // }) + } + + checkIfAlreadyHaveItemInDB(inventoryId: string, type: ItemType) { + // return db.inventoryItem.findFirst({ + // where: { inventoryId, type }, + // }) + } + + checkIfAlreadyHaveItem(type: ItemType) { + return this.items.find((item) => item.type === type) + } + + createItem(type: ItemType, amount: number) { + const item: IGameInventoryItem = { + id: createId(), + createdAt: new Date(), + updatedAt: new Date(), + type, + amount, + durability: 100, + inventoryId: "", + } + this.items.push(item) + } + + async updateFromDB() { + // const items = await db.inventoryItem.findMany({ + // where: { inventoryId: this.id }, + // }) + // this.items = items as IGameInventoryItem[] + } +} diff --git a/src/lib/game/common/poll.ts b/src/lib/game/common/poll.ts new file mode 100644 index 00000000..73d2d724 --- /dev/null +++ b/src/lib/game/common/poll.ts @@ -0,0 +1,49 @@ +import { createId } from "@paralleldrive/cuid2" +import { type IGameObjectPlayer, type IGamePoll, } from "$lib/game/types" +import { VoteAction } from "../actions/voteAction" +import type { GameScene } from "../scenes/gameScene" +import { getRandomInRange } from "$lib/random"; + +interface IPollOptions { + scene: GameScene + votesToSuccess: IGamePoll["votesToSuccess"] +} + +export class Poll implements IGamePoll { + public id: string + public status: IGamePoll["status"] + public action: IGamePoll["action"] + public votesToSuccess: IGamePoll["votesToSuccess"] + public votes: IGamePoll["votes"] = [] + + public scene: GameScene + + constructor({ votesToSuccess, scene }: IPollOptions) { + this.scene = scene + + this.id = createId() + this.status = "ACTIVE" + this.votesToSuccess = votesToSuccess + + this.action = new VoteAction({ poll: this }) + } + + public vote(player: IGameObjectPlayer): boolean { + if (this.votes.find((v) => v.id === player.id)) { + return false + } + + this.votes.push({ id: player.id, userName: player.userName }) + return true + } + + public generatePollId(): string { + const id = getRandomInRange(1, 9).toString() + for (const event of this.scene.eventService.events) { + if (event.poll?.action.command === `go ${id}`) { + return this.generatePollId() + } + } + return id + } +} diff --git a/src/lib/game/common/route.ts b/src/lib/game/common/route.ts new file mode 100644 index 00000000..8d7a278f --- /dev/null +++ b/src/lib/game/common/route.ts @@ -0,0 +1,156 @@ +import type { IGameChunk, IGameRoute, } from "$lib/game/types" +import { Flag } from "../objects" +import type { GameScene } from "../scenes/gameScene" + +interface IRoutePoint { + x: number + y: number +} + +interface IRouteArea { + startX: number + endX: number + startY: number + endY: number +} + +interface IRouteOptions { + scene: GameScene +} + +export class Route implements IGameRoute { + public startPoint!: IRoutePoint + public endPoint!: IRoutePoint + public chunks: IGameChunk[] = [] + + public scene: GameScene + public flags: Flag[] = [] + public areas: IRouteArea[] = [] + + constructor({ scene }: IRouteOptions) { + this.scene = scene + } + + public addChunk(chunk: IGameChunk) { + this.chunks.push({ + id: chunk.id, + type: chunk.type, + title: chunk.title, + center: chunk.center, + area: chunk.area, + }) + } + + public setEndPoint({ x, y }: IRoutePoint) { + this.endPoint = { x, y } + } + + public addFlag({ x, y }: IRoutePoint) { + const movementFlag = new Flag({ + scene: this.scene, + type: "WAGON_MOVEMENT", + x, + y, + }) + + const prevFlag = this.flags[this.flags.length - 1] + if (prevFlag) { + this.initArea(prevFlag, movementFlag) + } + + this.flags.push(movementFlag) + } + + public addGlobalFlag(end: IRoutePoint) { + const prevGlobalFlag = this.flags[this.flags.length - 1] + if (!prevGlobalFlag) { + return this.addFlag(end) + } + + this.generatePath({ x: prevGlobalFlag.x, y: prevGlobalFlag.y }, end) + this.addFlag({ x: end.x, y: end.y }) + } + + public getNextFlag() { + return this.flags[0] + } + + public removeFlag(flag: Flag) { + const index = this.flags.findIndex((f) => f.id === flag.id) + if (index >= 0) { + this.flags.splice(index, 1) + } + } + + public initArea(flag1: Flag, flag2: Flag) { + const offset = 150 + const halfOffset = offset / 2 + + const startX = Math.min(flag1.x, flag2.x) - offset + const endX = Math.max(flag1.x, flag2.x) + offset + + const startY = Math.min(flag1.y, flag2.y) - halfOffset + const endY = Math.max(flag1.y, flag2.y) + halfOffset + + const area = { + startX, + endX, + startY, + endY, + } + + this.areas.push(area) + } + + private isInArea(area: IRouteArea, point: IRoutePoint) { + return ( + area.startX < point.x && + point.x < area.endX && + area.startY < point.y && + point.y < area.endY + ) + } + + public checkIfPointIsOnWagonPath(point: IRoutePoint) { + for (const area of this.areas) { + if (this.isInArea(area, point)) { + return true + } + } + + return false + } + + generatePath(start: IRoutePoint, end: IRoutePoint) { + const pathDistance = Route.getDistanceBetween2Points(start, end) + console.log("path", pathDistance) + + const pointsCount = Math.round(pathDistance / 150) + 1 + console.log("points between", pointsCount) + + const stepX = Math.round((end.x - start.x) / pointsCount) + const stepY = Math.round((end.y - start.y) / pointsCount) + + let nowX = start.x + let nowY = start.y + + for (let i = 0; i < pointsCount; i++) { + nowX += stepX + nowY += stepY + this.addFlag({ x: nowX, y: nowY }) + } + } + + public static getDistanceBetween2Points( + point1: { + x: number + y: number + }, + point2: { + x: number + y: number + }, + ) { + return Math.sqrt((point1.x - point2.x) ** 2 + (point1.y - point2.y) ** 2) + } +} diff --git a/src/lib/game/common/skill.ts b/src/lib/game/common/skill.ts new file mode 100644 index 00000000..9700efca --- /dev/null +++ b/src/lib/game/common/skill.ts @@ -0,0 +1,81 @@ +import type { IGameSkill } from "$lib/game/types" + +interface ISkillOptions { + id: string +} + +export class Skill implements IGameSkill { + public id: string + public objectId: string | null = null + public type!: IGameSkill["type"] + + public lvl = 0 + public xp = 0 + public xpNextLvl = 0 + + constructor({ id }: ISkillOptions) { + this.id = id + } + + public async init() { + await this.readFromDB() + } + + public async addXp(increment = 1) { + this.xp += increment + + if (this.xp >= this.xpNextLvl) { + await this.lvlUpInDB() + await this.init() + return + } + + // return db.skill.update({ + // where: { id: this.id }, + // data: { xp: { increment } }, + // }) + } + + public lvlUpInDB() { + const xpNextLvl = Math.floor(this.xpNextLvl * 1.5) + // return db.skill.update({ + // where: { id: this.id }, + // data: { + // lvl: { increment: 1 }, + // xp: 0, + // xpNextLvl, + // }, + // }) + } + + async readFromDB() { + // const skill = await db.skill.findUnique({ + // where: { id: this.id }, + // }) + // if (!skill) { + // return + // } + // + // this.objectId = skill.objectId + // this.type = skill.type as IGameSkill["type"] + // this.lvl = skill.lvl + // this.xp = skill.xp + // this.xpNextLvl = skill.xpNextLvl + } + + public static async findAllInDB(objectId: string) { + // return db.skill.findMany({ + // where: { objectId }, + // }) + } + + public static createInDB(objectId: string, type: IGameSkill["type"]) { + // return db.skill.create({ + // data: { + // id: createId(), + // objectId, + // type, + // }, + // }) + } +} diff --git a/src/lib/game/components/buildingInterface.ts b/src/lib/game/components/buildingInterface.ts new file mode 100644 index 00000000..3c25c1e7 --- /dev/null +++ b/src/lib/game/components/buildingInterface.ts @@ -0,0 +1,124 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectBuilding } from "$lib/game/types" +import { GraphicsContainer } from "./graphicsContainer" + +export class BuildingInterface extends GraphicsContainer { + public building: IGameObjectBuilding + + public wood: number | undefined + public stone: number | undefined + + constructor(building: IGameObjectBuilding) { + super({ type: "INTERFACE" }) + + this.x = 0 + this.y = 0 + + this.building = building + + this.init() + } + + init() { + this.wood = + this.building.inventory.items.find((item) => item.type === "WOOD") + ?.amount ?? 0 + this.stone = + this.building.inventory.items.find((item) => item.type === "STONE") + ?.amount ?? 0 + + this.drawWood() + this.drawStone() + } + + rebuild() { + this.removeChild(...this.children) + this.drawWood() + this.drawStone() + } + + animate() { + this.visible = true + this.update() + } + + update() { + const wood = this.building.inventory.items.find( + (item) => item.type === "WOOD", + )?.amount + const stone = this.building.inventory.items.find( + (item) => item.type === "STONE", + )?.amount + + if (wood !== this.wood || stone !== this.stone) { + this.wood = wood + this.stone = stone + this.rebuild() + } + + this.wood = wood + this.stone = stone + } + + drawWood() { + if (!this.wood || this.wood <= 0) { + return + } + + const container = new GraphicsContainer({ type: "PLAYER_COINS" }) + + const woodSprite = Sprite.from("wood1") + woodSprite.width = 48 + woodSprite.height = 48 + + // const basicText = new Text({ + // text: this.wood, + // style: { + // fontSize: 16, + // fill: 0xfef3c7, + // align: "left", + // }, + // }) + // + // basicText.x = 14 + // basicText.y = 26 + + container.addChild(woodSprite) + + container.x = -50 + container.y = -74 + + this.addChild(container) + } + + drawStone() { + if (!this.stone || this.stone <= 0) { + return + } + + const container = new GraphicsContainer({ type: "PLAYER_COINS" }) + + const sprite = Sprite.from("stoneRes1") + sprite.width = 48 + sprite.height = 48 + + // const basicText = new Text({ + // text: this.stone, + // style: { + // fontSize: 16, + // fill: 0xfef3c7, + // align: "left", + // }, + // }) + // + // basicText.x = 14 + // basicText.y = 26 + + container.addChild(sprite) + + container.x = 4 + container.y = -74 + + this.addChild(container) + } +} diff --git a/src/lib/game/components/dialogueInterface.ts b/src/lib/game/components/dialogueInterface.ts new file mode 100644 index 00000000..ce552623 --- /dev/null +++ b/src/lib/game/components/dialogueInterface.ts @@ -0,0 +1,98 @@ +import { type Container, Graphics, Text } from "pixi.js" +import type { IGameObjectUnit } from "$lib/game/types" +import { GraphicsContainer } from "./graphicsContainer" + +export class DialogueInterface extends GraphicsContainer { + public unit: IGameObjectUnit + public messages: { id: string; text: string; isShowed: boolean }[] + public showingSpeed: number + + constructor(unit: IGameObjectUnit) { + super({ type: "INTERFACE" }) + + this.unit = unit + this.messages = [] + + this.showingSpeed = 0.0005 + + this.x = 0 + this.y = 0 + } + + create(message: { id: string; text: string }) { + const container = new GraphicsContainer({ type: "INTERFACE" }) + + const basicText = new Text({ + text: message.text, + style: { + fontFamily: "Noto Serif", + fontSize: 16, + fontWeight: "500", + fill: 0x694f62, + align: "left", + wordWrap: true, + wordWrapWidth: 300, + }, + }) + + const rectOffsetX = 8 + const rectOffsetY = 4 + const rectWidth = basicText.width + rectOffsetX * 2 + const rectHeight = basicText.height + rectOffsetY * 2 + + const graphics = new Graphics() + graphics.roundRect(-rectOffsetX, -rectOffsetY, rectWidth, rectHeight, 8) + graphics.fill(0xffffff) + + container.addChild(graphics, basicText) + + container.x = -container.width / 2 + 8 + container.y = -container.height - 75 + + this.addChild(container) + } + + remove(container: Container) { + return this.removeChild(container) + } + + animate() { + this.visible = true + this.zIndex = 0 + + // Add new messages to show + if (this.unit?.dialogue?.messages) { + for (const message of this.unit.dialogue.messages) { + const existed = this.messages.find((m) => m.id === message.id) + if (!existed) { + this.messages.push({ ...message, isShowed: false }) + } + } + } + + // If no active - create new show block! + const needToShowMessages = this.messages.filter((m) => !m.isShowed) + if (needToShowMessages.length > 0 && this.children.length <= 0) { + this.create(needToShowMessages[0]) + + needToShowMessages[0].isShowed = true + this.showingSpeed = this.getShowingSpeed( + needToShowMessages[0].text.length, + ) + } + + for (const container of this.children) { + container.visible = true + container.zIndex = 0 + container.alpha -= this.showingSpeed + + if (container.alpha <= 0.8) { + this.remove(container) + } + } + } + + getShowingSpeed(messageLength: number) { + return 0.0006 - (messageLength * 4) / 1000000 + } +} diff --git a/src/lib/game/components/fireParticlesContainer.ts b/src/lib/game/components/fireParticlesContainer.ts new file mode 100644 index 00000000..6223d125 --- /dev/null +++ b/src/lib/game/components/fireParticlesContainer.ts @@ -0,0 +1,66 @@ +import { Sprite } from "pixi.js" +import { GraphicsContainer } from "./graphicsContainer" +import { getRandomInRange } from "$lib/random"; + +interface IFireParticlesContainerOptions { + x: number + y: number + areaWidth: number +} + +export class FireParticlesContainer extends GraphicsContainer { + public areaWidth: number + public offset = 0 + + constructor({ x, y, areaWidth }: IFireParticlesContainerOptions) { + super({ type: "FIRE_PARTICLE", direction: "LEFT" }) + + this.x = x + this.y = y + this.areaWidth = areaWidth + } + + createRandom() { + const sprite = Sprite.from(this.getRandomSpriteIndex()) + sprite.anchor.set(0.5, 1) + + const half = this.width / 2 + sprite.x = getRandomInRange(-half, half) + sprite.scale = getRandomInRange(10, 20) / 10 + + this.addChild(sprite) + } + + getRandomSpriteIndex() { + const random = getRandomInRange(1, 1000) + if (random <= 500) { + return "fireParticle1" + } + if (random <= 750) { + return "fireParticle2" + } + return "fireParticle3" + } + + animate(power = 1) { + this.offset -= power + 1 + + const activeNow = power * 8 + 1 + const canCreate = this.children.length < activeNow && this.offset <= 0 + if (canCreate) { + this.createRandom() + this.offset = power * getRandomInRange(90, 110) + 3 + } + + for (const container of this.children) { + container.visible = true + + container.y -= 0.28 + container.alpha -= 0.008 + + if (container.alpha <= 0) { + this.removeChild(container) + } + } + } +} diff --git a/src/lib/game/components/graphicsContainer.ts b/src/lib/game/components/graphicsContainer.ts new file mode 100644 index 00000000..e4bf3da0 --- /dev/null +++ b/src/lib/game/components/graphicsContainer.ts @@ -0,0 +1,38 @@ +import { Container, Sprite } from "pixi.js" +import type { + GraphicsContainerType, + IGameObjectDirection, +} from "$lib/game/types" + +interface IGraphicsContainerOptions { + type: GraphicsContainerType + direction?: IGameObjectDirection +} + +export class GraphicsContainer extends Container { + public type: GraphicsContainerType + public direction: IGameObjectDirection = "RIGHT" + + constructor({ type, direction }: IGraphicsContainerOptions) { + super() + this.type = type + + if (direction) { + this.direction = direction + } + } + + static createWithSprite( + spriteIndex: string, + direction: IGameObjectDirection, + type: GraphicsContainerType, + ) { + const sprite = Sprite.from(spriteIndex) + sprite.anchor.set(0.5, 1) + + const container = new GraphicsContainer({ type, direction }) + container.addChild(sprite) + + return container + } +} diff --git a/src/lib/game/components/unitHairContainer.ts b/src/lib/game/components/unitHairContainer.ts new file mode 100644 index 00000000..33f8f998 --- /dev/null +++ b/src/lib/game/components/unitHairContainer.ts @@ -0,0 +1,65 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectDirection, IGameObjectUnit, } from "$lib/game/types" +import { GraphicsContainer } from "./graphicsContainer" + +interface IUnitHairContainerOptions { + direction: IGameObjectDirection + visual: IGameObjectUnit["visual"]["hairstyle"] +} + +export class UnitHairContainer extends GraphicsContainer { + public visual: IGameObjectUnit["visual"]["hairstyle"] + + constructor({ direction, visual }: IUnitHairContainerOptions) { + super({ type: "UNIT_HAIR", direction }) + this.visual = visual + } + + static create( + spriteIndex: string, + direction: IGameObjectDirection, + visual: IGameObjectUnit["visual"]["hairstyle"], + ) { + const sprite = Sprite.from(spriteIndex) + sprite.anchor.set(0.5, 1) + + if (direction === "LEFT") { + // Flip horizontally + sprite.scale.x = -1 + } + + const container = new UnitHairContainer({ direction, visual }) + container.addChild(sprite) + + return container + } + + static createWithBothDirections( + spriteIndex: string, + visual: IGameObjectUnit["visual"]["hairstyle"], + ) { + const containers = [] + + containers.push(UnitHairContainer.create(spriteIndex, "LEFT", visual)) + containers.push(UnitHairContainer.create(spriteIndex, "RIGHT", visual)) + + return containers + } + + static getAll() { + return [ + ...UnitHairContainer.createWithBothDirections( + "unitHairClassic", + "CLASSIC", + ), + ...UnitHairContainer.createWithBothDirections( + "unitHairCoalLong", + "COAL_LONG", + ), + ...UnitHairContainer.createWithBothDirections( + "unitHairOrangeWithBeard", + "ORANGE_WITH_BEARD", + ), + ] + } +} diff --git a/src/lib/game/components/unitHeadContainer.ts b/src/lib/game/components/unitHeadContainer.ts new file mode 100644 index 00000000..e75b7990 --- /dev/null +++ b/src/lib/game/components/unitHeadContainer.ts @@ -0,0 +1,52 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectDirection, IGameObjectUnit, } from "$lib/game/types" +import { GraphicsContainer } from "./graphicsContainer" + +interface IUnitHeadContainerOptions { + direction: IGameObjectDirection + visual: IGameObjectUnit["visual"]["head"] +} + +export class UnitHeadContainer extends GraphicsContainer { + public visual: IGameObjectUnit["visual"]["head"] + + constructor({ direction, visual }: IUnitHeadContainerOptions) { + super({ type: "UNIT_HEAD", direction }) + this.visual = visual + } + + static create( + spriteIndex: string, + direction: IGameObjectDirection, + visual: IGameObjectUnit["visual"]["head"], + ) { + const sprite = Sprite.from(spriteIndex) + sprite.anchor.set(0.5, 1) + + if (direction === "LEFT") { + // Flip horizontally + sprite.scale.x = -1 + } + + const container = new UnitHeadContainer({ direction, visual }) + container.addChild(sprite) + + return container + } + + static createWithBothDirections( + spriteIndex: string, + visual: IGameObjectUnit["visual"]["head"], + ) { + const containers = [] + + containers.push(UnitHeadContainer.create(spriteIndex, "LEFT", visual)) + containers.push(UnitHeadContainer.create(spriteIndex, "RIGHT", visual)) + + return containers + } + + static getAll() { + return [...UnitHeadContainer.createWithBothDirections("unitHead1", "1")] + } +} diff --git a/src/lib/game/components/unitInterface.ts b/src/lib/game/components/unitInterface.ts new file mode 100644 index 00000000..e2d58059 --- /dev/null +++ b/src/lib/game/components/unitInterface.ts @@ -0,0 +1,317 @@ +import { Graphics, Sprite, Text } from "pixi.js" +import type { IGameObjectUnit } from "$lib/game/types" +import { GraphicsContainer } from "./graphicsContainer" + +export class UnitInterface extends GraphicsContainer { + public children: GraphicsContainer[] = [] + public unit: IGameObjectUnit + + public userName = "" + public coins = 0 + public wood = 0 + public stone = 0 + + public showInHandItemIndex = 0 + public showInHandTimerSeconds = 0 + public showToolTimerSeconds = 0 + public showWood = false + public showStone = false + + public haveAxe = false + public showAxe = false + public havePickaxe = false + public showPickaxe = false + + constructor(unit: IGameObjectUnit) { + super({ type: "INTERFACE" }) + + this.x = -15 + this.y = 5 + + this.unit = unit + + this.init() + } + + init() { + this.coins = this.unit.coins + this.wood = + this.unit.inventory.items.find((item) => item.type === "WOOD")?.amount ?? + 0 + this.stone = + this.unit.inventory.items.find((item) => item.type === "STONE")?.amount ?? + 0 + + this.haveAxe = !!this.unit.inventory.items.find( + (item) => item.type === "AXE", + ) + this.havePickaxe = !!this.unit.inventory.items.find( + (item) => item.type === "PICKAXE", + ) + + this.drawUserName() + this.drawWood() + this.drawStone() + this.drawAxe() + this.drawPickaxe() + + this.setShowInHandTimer() + this.setShowToolTimer() + } + + rebuild() { + this.removeChild(...this.children) + this.drawUserName() + this.drawWood() + this.drawStone() + this.drawAxe() + this.drawPickaxe() + this.showInHandTimerSeconds = 0 + } + + update() { + const userName = this.unit.userName + const wood = + this.unit.inventory.items.find((item) => item.type === "WOOD")?.amount ?? + 0 + const stone = + this.unit.inventory.items.find((item) => item.type === "STONE")?.amount ?? + 0 + const haveAxe = !!this.unit.inventory.items.find( + (item) => item.type === "AXE", + ) + const havePickaxe = !!this.unit.inventory.items.find( + (item) => item.type === "PICKAXE", + ) + + if ( + userName !== this.userName || + this.unit.coins !== this.coins || + wood !== this.wood || + stone !== this.stone || + haveAxe !== this.haveAxe || + havePickaxe !== this.havePickaxe + ) { + this.userName = userName + this.coins = this.unit.coins + this.wood = wood + this.stone = stone + this.haveAxe = haveAxe + this.havePickaxe = havePickaxe + this.rebuild() + } + + this.userName = this.unit.userName + this.coins = this.unit.coins + this.wood = wood + this.stone = stone + this.haveAxe = haveAxe + this.havePickaxe = havePickaxe + } + + animate() { + this.visible = true + this.update() + + for (const container of this.children) { + container.visible = true + + if (container.type === "PLAYER_COINS") { + if (!this.coins) { + container.visible = false + } + } + + if (container.type === "PLAYER_WOOD") { + if (this.wood <= 0 || !this.showWood) { + container.visible = false + } + } + + if (container.type === "PLAYER_STONE") { + if (this.stone <= 0 || !this.showStone) { + container.visible = false + } + } + + if (container.type === "PLAYER_AXE") { + if (!this.haveAxe || !this.showAxe) { + container.visible = false + } + } + + if (container.type === "PLAYER_PICKAXE") { + if (!this.havePickaxe || !this.showPickaxe) { + container.visible = false + } + } + } + } + + drawUserName() { + if (!this.unit.userName) { + return + } + + const container = new GraphicsContainer({ type: "INTERFACE" }) + + const basicText = new Text({ + text: this.unit.userName, + style: { + fontFamily: "Noto Serif", + fontSize: 14, + fontWeight: "600", + fill: 0x451a03, + align: "center", + }, + }) + + const graphics = new Graphics() + graphics.roundRect(-6, -2, basicText.width + 12, basicText.height + 4, 0) + graphics.fill(0xfef3c7) + + container.addChild(graphics, basicText) + + container.x = -container.width / 2 + 24 + container.y = -84 + + this.addChild(container) + } + + drawWood() { + const container = new GraphicsContainer({ type: "PLAYER_WOOD" }) + + const woodSprite = Sprite.from("wood1") + woodSprite.width = 32 + woodSprite.height = 32 + + container.addChild(woodSprite) + + container.x = -18 + container.y = -48 + + this.addChild(container) + } + + drawStone() { + const container = new GraphicsContainer({ type: "PLAYER_STONE" }) + + const woodSprite = Sprite.from("stoneRes1") + woodSprite.width = 32 + woodSprite.height = 32 + + container.addChild(woodSprite) + + container.x = -18 + container.y = -48 + + this.addChild(container) + } + + drawAxe() { + const container = new GraphicsContainer({ type: "PLAYER_AXE" }) + + const sprite = Sprite.from("toolAxe1") + sprite.width = 64 + sprite.height = 64 + + container.addChild(sprite) + + container.x = -10 + container.y = -68 + + this.addChild(container) + } + + drawPickaxe() { + const container = new GraphicsContainer({ type: "PLAYER_PICKAXE" }) + + const sprite = Sprite.from("toolPickaxe1") + sprite.width = 64 + sprite.height = 64 + + container.addChild(sprite) + + container.x = -10 + container.y = -68 + + this.addChild(container) + } + + showWoodInHand() { + this.showWood = true + } + + showStoneInHand() { + this.showStone = true + } + + showAxeInHand() { + this.showAxe = true + this.showToolTimerSeconds = 5 + } + + showPickaxeInHand() { + this.showPickaxe = true + this.showToolTimerSeconds = 5 + } + + setShowInHandTimer() { + return setInterval(() => { + if (this.showInHandTimerSeconds <= 0) { + this.showNextItem() + return + } + + this.showInHandTimerSeconds -= 1 + }, 1000) + } + + setShowToolTimer() { + return setInterval(() => { + if (this.showToolTimerSeconds <= 0) { + this.showAxe = false + this.showPickaxe = false + return + } + + this.showToolTimerSeconds -= 1 + }) + } + + getResourcesArray() { + return this.unit.inventory.items.filter( + (item) => item.type === "WOOD" || item.type === "STONE", + ) + } + + showNextItem() { + const items = this.getResourcesArray() + if (!items) { + return + } + + if (items.length < this.showInHandItemIndex + 1) { + this.showInHandItemIndex = 0 + } + + this.showWood = false + this.showStone = false + this.showInHandTimerSeconds = 5 + + if (!items[this.showInHandItemIndex]?.type) { + return + } + + switch (items[this.showInHandItemIndex].type) { + case "WOOD": + this.showWoodInHand() + break + case "STONE": + this.showStoneInHand() + break + } + + this.showInHandItemIndex += 1 + } +} diff --git a/src/lib/game/components/unitTopContainer.ts b/src/lib/game/components/unitTopContainer.ts new file mode 100644 index 00000000..c9faa554 --- /dev/null +++ b/src/lib/game/components/unitTopContainer.ts @@ -0,0 +1,61 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectDirection, IGameObjectUnit, } from "$lib/game/types" +import { GraphicsContainer } from "./graphicsContainer" + +interface IUnitTopContainerOptions { + direction: IGameObjectDirection + visual: IGameObjectUnit["visual"]["top"] +} + +export class UnitTopContainer extends GraphicsContainer { + public visual: IGameObjectUnit["visual"]["top"] + + constructor({ direction, visual }: IUnitTopContainerOptions) { + super({ type: "UNIT_TOP", direction }) + this.visual = visual + } + + static create( + spriteIndex: string, + direction: IGameObjectDirection, + visual: IGameObjectUnit["visual"]["top"], + ) { + const sprite = Sprite.from(spriteIndex) + sprite.anchor.set(0.5, 1) + + if (direction === "LEFT") { + // Flip horizontally + sprite.scale.x = -1 + } + + const container = new UnitTopContainer({ direction, visual }) + container.addChild(sprite) + + return container + } + + static createWithBothDirections( + spriteIndex: string, + visual: IGameObjectUnit["visual"]["top"], + ) { + const containers = [] + + containers.push(UnitTopContainer.create(spriteIndex, "LEFT", visual)) + containers.push(UnitTopContainer.create(spriteIndex, "RIGHT", visual)) + + return containers + } + + static getAll() { + return [ + ...UnitTopContainer.createWithBothDirections("violetTop", "VIOLET_SHIRT"), + ...UnitTopContainer.createWithBothDirections("blackTop", "BLACK_SHIRT"), + ...UnitTopContainer.createWithBothDirections("greenTop", "GREEN_SHIRT"), + ...UnitTopContainer.createWithBothDirections("blueTop", "BLUE_SHIRT"), + ...UnitTopContainer.createWithBothDirections( + "darkSilverTop", + "DARK_SILVER_SHIRT", + ), + ] + } +} diff --git a/src/lib/game/components/wagonCargoContainer.ts b/src/lib/game/components/wagonCargoContainer.ts new file mode 100644 index 00000000..bceba39b --- /dev/null +++ b/src/lib/game/components/wagonCargoContainer.ts @@ -0,0 +1,22 @@ +import { Sprite } from "pixi.js" +import { GraphicsContainer } from "./graphicsContainer" + +export class WagonCargoContainer extends GraphicsContainer { + constructor() { + super({ type: "WAGON_CARGO", direction: "RIGHT" }) + } + + static create() { + const chest1 = Sprite.from("chest1") + chest1.anchor.set(0.5, 1) + chest1.visible = false + + const container = new WagonCargoContainer() + container.addChild(chest1) + + container.x = 72 + container.y = -56 + + return container + } +} diff --git a/src/lib/game/components/wagonEngineCloudsContainer.ts b/src/lib/game/components/wagonEngineCloudsContainer.ts new file mode 100644 index 00000000..e2560490 --- /dev/null +++ b/src/lib/game/components/wagonEngineCloudsContainer.ts @@ -0,0 +1,66 @@ +import { type Container, Sprite } from "pixi.js" +import { GraphicsContainer } from "./graphicsContainer" +import { getRandomInRange } from "$lib/random"; + +export class WagonEngineCloudsContainer extends GraphicsContainer { + public offset = 0 + + constructor() { + super({ type: "WAGON_ENGINE_CLOUD", direction: "LEFT" }) + + this.x = -106 + this.y = -118 + } + + createRandom() { + const sprite = Sprite.from(this.getRandomSpriteIndex()) + sprite.anchor.set(0.5, 1) + sprite.scale = 0.75 + sprite.visible = false + + this.addChild(sprite) + } + + getRandomSpriteIndex() { + const random = getRandomInRange(1, 1000) + if (random <= 500) { + return "wagonEngineCloud1" + } + if (random <= 750) { + return "wagonEngineCloud2" + } + if (random <= 995) { + return "wagonEngineCloud3" + } + return "wagonEngineCloud4" + } + + remove(container: Container) { + return this.removeChild(container) + } + + animate(speed: number) { + this.offset -= speed + 1 + + const cloudsActive = speed * 8 + 1 + const canCreateCloud = + this.children.length < cloudsActive && this.offset <= 0 + if (canCreateCloud) { + this.createRandom() + this.offset = speed * getRandomInRange(170, 190) + 3 + } + + for (const container of this.children) { + container.visible = true + + container.x -= speed / 3 + 0.07 + container.y -= 0.06 + container.scale = 0.75 + container.alpha -= 0.005 + + if (container.alpha <= 0) { + this.remove(container) + } + } + } +} diff --git a/src/lib/game/components/wagonEngineContainer.ts b/src/lib/game/components/wagonEngineContainer.ts new file mode 100644 index 00000000..20bebea9 --- /dev/null +++ b/src/lib/game/components/wagonEngineContainer.ts @@ -0,0 +1,23 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectDirection } from "$lib/game/types" +import { GraphicsContainer } from "./graphicsContainer" + +interface IWagonEngineContainerOptions { + direction: IGameObjectDirection +} + +export class WagonEngineContainer extends GraphicsContainer { + constructor({ direction }: IWagonEngineContainerOptions) { + super({ type: "WAGON_ENGINE", direction }) + } + + static create(spriteIndex: string, direction: IGameObjectDirection) { + const sprite = Sprite.from(spriteIndex) + sprite.anchor.set(0.5, 1) + + const container = new WagonEngineContainer({ direction }) + container.addChild(sprite) + + return container + } +} diff --git a/src/lib/game/components/wagonFuelBoxContainer.ts b/src/lib/game/components/wagonFuelBoxContainer.ts new file mode 100644 index 00000000..5042cdcc --- /dev/null +++ b/src/lib/game/components/wagonFuelBoxContainer.ts @@ -0,0 +1,25 @@ +import { Sprite } from "pixi.js" +import { GraphicsContainer } from "./graphicsContainer" +import { WagonFuelContainer } from "./wagonFuelContainer" + +export class WagonFuelBoxContainer extends GraphicsContainer { + public children: Sprite[] | WagonFuelContainer[] = [] + + constructor() { + super({ type: "WAGON_FUEL", direction: "RIGHT" }) + } + + static create() { + const storage = Sprite.from("wagonFuelContainer1") + storage.anchor.set(0.5, 1) + + const container = new WagonFuelBoxContainer() + const fuel = new WagonFuelContainer() + container.addChild(storage, fuel) + + container.x = 0 + container.y = -18 + + return container + } +} diff --git a/src/lib/game/components/wagonFuelContainer.ts b/src/lib/game/components/wagonFuelContainer.ts new file mode 100644 index 00000000..125ae9a7 --- /dev/null +++ b/src/lib/game/components/wagonFuelContainer.ts @@ -0,0 +1,31 @@ +import { Sprite } from "pixi.js" +import { GraphicsContainer } from "./graphicsContainer" +import { getRandomInRange } from "$lib/random"; + +export class WagonFuelContainer extends GraphicsContainer { + constructor() { + super({ type: "WAGON_FUEL", direction: "RIGHT" }) + + this.initFuelContainer() + } + + initFuelContainer() { + const offsetX = 76 + const offsetY = 52 + + for (let i = 1; i < 10; i++) { + const sprite = this.getRandomFuelSprite() + sprite.x = i * 12 - offsetX + sprite.y = -offsetY + this.addChild(sprite) + } + } + + getRandomFuelSprite() { + const random = getRandomInRange(1, 2) + if (random === 1) { + return Sprite.from("wagonFuel1") + } + return Sprite.from("wagonFuel2") + } +} diff --git a/src/lib/game/components/wagonWheelContainer.ts b/src/lib/game/components/wagonWheelContainer.ts new file mode 100644 index 00000000..3111c96a --- /dev/null +++ b/src/lib/game/components/wagonWheelContainer.ts @@ -0,0 +1,31 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectDirection } from "$lib/game/types" +import { GraphicsContainer } from "./graphicsContainer" + +interface IWagonWheelContainerOptions { + direction: IGameObjectDirection + side: "LEFT" | "RIGHT" +} + +export class WagonWheelContainer extends GraphicsContainer { + public side: IWagonWheelContainerOptions["side"] + + constructor({ direction, side }: IWagonWheelContainerOptions) { + super({ type: "WAGON_WHEEL", direction }) + this.side = side + } + + static create( + spriteIndex: string, + direction: IGameObjectDirection, + side: IWagonWheelContainerOptions["side"], + ) { + const sprite = Sprite.from(spriteIndex) + sprite.anchor.set(0.5, 0.5) + + const container = new WagonWheelContainer({ direction, side }) + container.addChild(sprite) + + return container + } +} diff --git a/src/lib/game/game.ts b/src/lib/game/game.ts new file mode 100644 index 00000000..21a16704 --- /dev/null +++ b/src/lib/game/game.ts @@ -0,0 +1,321 @@ +import { Application, Container } from "pixi.js" +import type { + IGameObject, + IGameObjectMechanic, + IGameObjectPlayer, + IGameObjectRaider, + IGameObjectTrader, + WebSocketMessage, + GameSceneType, +} from "$lib/game/types" +import { type GameObject, type Wagon } from "./objects" +import { Mechanic, Player, Raider, Trader } from "./objects/units" +import type { GameScene } from "./scenes/gameScene" +import { MovingScene } from "./scenes/movingScene" +import { AssetsManager, AudioManager } from "./utils" +import { BackgroundGenerator } from "./utils/generators/background" + +export class Game extends Container { + public children: GameObject[] = [] + public app!: Application + public audio!: AudioManager + public scene!: GameScene + public bg!: BackgroundGenerator + + public tick = 0 + public cameraOffsetX = 0 + public cameraMovementSpeedX = 0.008 + public cameraOffsetY = 0 + public cameraMovementSpeedY = 0.008 + public cameraX = 0 + public cameraY = 0 + public cameraPerfectX = 0 + public cameraPerfectY = 0 + + constructor() { + super() + + this.app = new Application() + this.audio = new AudioManager() + this.bg = new BackgroundGenerator(this.app) + } + + public async init() { + await this.app.init({ + background: "#239063", + antialias: false, + roundPixels: false, + resolution: 1, + }) + + await AssetsManager.init() + + // this.audio.playSound("FOREST_BACKGROUND") + + //const bg = this.bg.getGeneratedBackgroundTilingSprite() + //bg.width = 10000 + //bg.height = 10000 + //this.app.stage.addChild(bg) + + this.app.stage.addChild(this) + + // WebSocketManager.init(this) + + this.scene = new MovingScene({ game: this }) + + this.app.ticker.add(() => { + this.tick = this.app.ticker.FPS + + this.scene.live() + + this.animateObjects() + this.removeDestroyedObjects() + + const wagon = this.scene.wagonService.wagon + this.changeCameraPosition(wagon) + + this.moveCamera() + }) + } + + public async play() { + // setInterval(() => { + // console.log("FPS", this.app.ticker.FPS) + // console.log("Objects", this.children.length) + // }, 1000) + } + + public destroy() { + this.audio.destroy() + this.app.destroy() + + super.destroy(); + } + + private changeCameraPosition(wagon: Wagon) { + const columnWidth = this.app.screen.width / 6 + const rowHeight = this.app.screen.height / 6 + + let leftPadding = + wagon.direction === "LEFT" ? columnWidth * 4 : columnWidth * 2 + let topPadding = rowHeight * 3 + + if (wagon.speedPerSecond === 0) { + leftPadding = columnWidth * 3 + + if (wagon.state === "IDLE" && !wagon.cargoType) { + // At Village stop + leftPadding = columnWidth + topPadding = rowHeight * 4 + } + } + + this.cameraPerfectX = -wagon.x + leftPadding + this.cameraPerfectY = -wagon.y + topPadding + + // If first load + if (Math.abs(-wagon.x - this.cameraX) > 3000) { + this.cameraX = this.cameraPerfectX + } + if (Math.abs(-wagon.y - this.cameraY) > 3000) { + this.cameraY = this.cameraPerfectY + } + } + + private moveCamera() { + const cameraMaxSpeed = 0.2 + const bufferX = Math.abs(this.cameraPerfectX - this.cameraX) + const moduleX = this.cameraPerfectX - this.cameraX > 0 ? 1 : -1 + const addToX = bufferX > cameraMaxSpeed ? cameraMaxSpeed : bufferX + + if (this.cameraX !== this.cameraPerfectX) { + this.cameraX += addToX * moduleX + } + + const bufferY = Math.abs(this.cameraPerfectY - this.cameraY) + const moduleY = this.cameraPerfectY - this.cameraY > 0 ? 1 : -1 + const addToY = bufferY > cameraMaxSpeed ? cameraMaxSpeed : bufferY + + if (this.cameraY !== this.cameraPerfectY) { + this.cameraY += addToY * moduleY + } + + // if (Math.abs(this.cameraOffsetX) >= 20) { + // this.cameraMovementSpeedX *= -1 + // } + // this.cameraOffsetX += this.cameraMovementSpeedX + // + // if (Math.abs(this.cameraOffsetY) >= 30) { + // this.cameraMovementSpeedY *= -1 + // } + // this.cameraOffsetY += this.cameraMovementSpeedY + + if (this.parent) { + this.parent.x = this.cameraX + this.parent.y = this.cameraY + } + } + + rebuildScene() { + this.removeChild(...this.children) + } + + findObject(id: string) { + return this.children.find((obj) => obj.id === id) + } + + public initScene(scene: GameSceneType) { + if (this.scene) { + this.scene.destroy() + } + + if (scene === "MOVING") { + this.scene = new MovingScene({ game: this }) + return + } + } + + initPlayer(object: IGameObjectPlayer) { + const player = new Player({ game: this, object }) + this.addChild(player) + } + + updatePlayer(object: IGameObjectPlayer) { + const player = this.findObject(object.id) + if (player instanceof Player) { + player.update(object) + } + } + + initTrader(object: IGameObjectTrader) { + const unit = new Trader({ game: this, object }) + this.addChild(unit) + } + + updateTrader(object: IGameObjectTrader) { + const unit = this.findObject(object.id) + if (unit instanceof Trader) { + unit.update(object) + } + } + + initMechanic(object: IGameObjectMechanic) { + const unit = new Mechanic({ game: this, object }) + this.addChild(unit) + } + + updateMechanic(object: IGameObjectMechanic) { + const unit = this.findObject(object.id) + if (unit instanceof Mechanic) { + unit.update(object) + } + } + + initRaider(object: IGameObjectRaider) { + const raider = new Raider({ game: this, object }) + this.addChild(raider) + } + + updateRaider(object: IGameObjectRaider) { + const raider = this.findObject(object.id) + if (raider instanceof Raider) { + raider.update(object) + } + } + + public checkIfThisFlagIsTarget(id: string) { + for (const obj of this.children) { + if (obj.target?.id === id) { + return true + } + } + } + + animateObjects() { + for (const object of this.children) { + object?.animate() + object?.live() + } + } + + removeDestroyedObjects() { + for (const object of this.children) { + if (object.state === "DESTROYED") { + const index = this.children.indexOf(object) + this.children.splice(index, 1) + return + } + } + } + + handleMessage(message: WebSocketMessage) { + if (message.object) { + this.handleMessageObject(message.object) + } + if (message.event) { + this.handleMessageEvent(message.event) + } + } + + handleMessageObject(object: Partial) { + if (!object.id) { + return + } + + const obj = this.findObject(object.id) + if (!obj) { + if (object.entity === "PLAYER") { + this.initPlayer(object as IGameObjectPlayer) + return + } + if (object.entity === "TRADER") { + this.initTrader(object as IGameObjectTrader) + return + } + if (object.entity === "MECHANIC") { + this.initMechanic(object as IGameObjectMechanic) + return + } + if (object.entity === "RAIDER") { + this.initRaider(object as IGameObjectRaider) + return + } + return + } + + if (object.entity === "PLAYER") { + this.updatePlayer(object as IGameObjectPlayer) + return + } + if (object.entity === "TRADER") { + this.updateTrader(object as IGameObjectTrader) + return + } + if (object.entity === "MECHANIC") { + this.updateMechanic(object as IGameObjectMechanic) + return + } + if (object.entity === "RAIDER") { + this.updateRaider(object as IGameObjectRaider) + return + } + } + + handleMessageEvent(event: WebSocketMessage["event"]) { + if (event === "RAID_STARTED") { + this.audio.playSound("MARCHING_WITH_HORNS") + } + if (event === "GROUP_FORM_STARTED") { + this.audio.playSound("MARCHING_WITH_HORNS") + } + if (event === "MAIN_QUEST_STARTED") { + this.audio.playSound("MARCHING_WITH_HORNS") + } + if (event === "SCENE_CHANGED") { + this.rebuildScene() + } + if (event === "IDEA_CREATED") { + this.audio.playSound("YEAH") + } + } +} diff --git a/src/lib/game/index.ts b/src/lib/game/index.ts new file mode 100644 index 00000000..cde3bfa9 --- /dev/null +++ b/src/lib/game/index.ts @@ -0,0 +1,4 @@ +// const game = new Game() +// void game.play() +// +// export { game } diff --git a/src/lib/game/objects/area.ts b/src/lib/game/objects/area.ts new file mode 100644 index 00000000..e7740d4d --- /dev/null +++ b/src/lib/game/objects/area.ts @@ -0,0 +1,42 @@ +import type { IGameObjectArea } from "$lib/game/types" +import type { GameScene } from "../scenes/gameScene.ts" +import { GameObject } from "./gameObject.ts" + +interface IAreaOptions { + scene: GameScene + theme: IGameObjectArea["theme"] + area: IGameObjectArea["area"] +} + +export class Area extends GameObject implements IGameObjectArea { + public theme: IGameObjectArea["theme"] + public area: IGameObjectArea["area"] + + constructor({ scene, theme, area }: IAreaOptions) { + const x = area.startX + const y = area.startY + + super({ scene, x, y }) + + this.theme = theme + this.area = area + + this.initGraphics() + } + + private initGraphics() { + this.scene.game.bg.changePaletteByTheme(this.theme) + + const bg = this.scene.game.bg.getGeneratedBackgroundTilingSprite() + bg.width = this.area.endX - this.area.startX + bg.height = this.area.endY - this.area.startY + + this.addChild(bg) + } + + animate() { + super.animate() + + this.zIndex = -1 + } +} diff --git a/src/lib/game/objects/buildings/building.ts b/src/lib/game/objects/buildings/building.ts new file mode 100644 index 00000000..8c598669 --- /dev/null +++ b/src/lib/game/objects/buildings/building.ts @@ -0,0 +1,48 @@ +import { createId } from "@paralleldrive/cuid2" +import type { IGameObjectBuilding, ItemType, } from "$lib/game/types" +import { Inventory } from "../../common" +import type { GameScene } from "../../scenes/gameScene.ts" +import { GameObject } from "../gameObject.ts" + +interface IBuildingOptions { + scene: GameScene + x: number + y: number +} + +export class Building extends GameObject implements IGameObjectBuilding { + public inventory!: Inventory + + constructor({ scene, x, y }: IBuildingOptions) { + super({ scene, x, y }) + + this.state = "IDLE" + this.initInventory() + } + + public animate() { + super.animate() + + this.zIndex = Math.round(this.y - 5) + + if (this.state === "DESTROYED") { + this.visible = false + } + } + + private initInventory() { + this.inventory = new Inventory({ + objectId: this.id, + id: createId(), + saveInDb: false, + }) + } + + public getItemByType(type: ItemType) { + if (!this.inventory?.items) { + return + } + + return this.inventory.items.find((item) => item.type === type) + } +} diff --git a/src/lib/game/objects/buildings/campfire.ts b/src/lib/game/objects/buildings/campfire.ts new file mode 100644 index 00000000..188c03e8 --- /dev/null +++ b/src/lib/game/objects/buildings/campfire.ts @@ -0,0 +1,58 @@ +import { type AnimatedSprite, Sprite } from "pixi.js" +import type { IGameBuildingCampfire } from "$lib/game/types" +import { FireParticlesContainer } from "../../components/fireParticlesContainer" +import type { GameScene } from "../../scenes/gameScene.ts" +import { AssetsManager } from "../../utils" +import { Building } from "./building" + +interface ICampfireOptions { + scene: GameScene + x: number + y: number +} + +export class Campfire extends Building implements IGameBuildingCampfire { + private fireAnimation!: AnimatedSprite + private fireParticles!: FireParticlesContainer + + constructor({ scene, x, y }: ICampfireOptions) { + super({ scene, x, y }) + + this.initGraphics() + } + + public animate() { + super.animate() + + this.fireAnimation.animationSpeed = 0.08 + this.fireAnimation.play() + + this.fireParticles.animate(1) + + this.handleSoundByState() + } + + private initGraphics() { + const sprite = Sprite.from("campfire1") + sprite.anchor.set(0.5, 1) + + this.fireAnimation = AssetsManager.getAnimatedSpriteCampfire() + this.fireParticles = new FireParticlesContainer({ + x: 0, + y: -40, + areaWidth: 40, + }) + + this.addChild(sprite, this.fireAnimation, this.fireParticles) + } + + private handleSoundByState() { + if (!this.visible) { + return + } + + if (this.state === "IDLE") { + this.scene.game.audio.playSound("FIRE_BURN") + } + } +} diff --git a/src/lib/game/objects/buildings/constructionArea.ts b/src/lib/game/objects/buildings/constructionArea.ts new file mode 100644 index 00000000..e6f37b2e --- /dev/null +++ b/src/lib/game/objects/buildings/constructionArea.ts @@ -0,0 +1,28 @@ +import { Sprite } from "pixi.js" +import type { IGameBuildingConstructionArea } from "$lib/game/types" +import type { GameScene } from "../../scenes/gameScene" +import { Building } from "./building" + +interface IConstructionAreaOptions { + scene: GameScene + x: number + y: number +} + +export class ConstructionArea + extends Building + implements IGameBuildingConstructionArea { + constructor({ scene, x, y }: IConstructionAreaOptions) { + super({ scene, x, y }) + + this.initGraphics() + } + + private initGraphics() { + const sprite = Sprite.from("constructionArea1") + if (sprite) { + sprite.anchor.set(0.5, 0.92) + this.addChild(sprite) + } + } +} diff --git a/src/lib/game/objects/buildings/store.ts b/src/lib/game/objects/buildings/store.ts new file mode 100644 index 00000000..851e771c --- /dev/null +++ b/src/lib/game/objects/buildings/store.ts @@ -0,0 +1,26 @@ +import { Sprite } from "pixi.js" +import type { IGameBuildingStore } from "$lib/game/types" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Building } from "./building" + +interface IStoreOptions { + scene: GameScene + x: number + y: number +} + +export class Store extends Building implements IGameBuildingStore { + constructor({ scene, x, y }: IStoreOptions) { + super({ scene, x, y }) + + this.initGraphics() + } + + private initGraphics() { + const sprite = Sprite.from("store1") + if (sprite) { + sprite.anchor.set(0.5, 1) + this.addChild(sprite) + } + } +} diff --git a/src/lib/game/objects/buildings/wagonStop.ts b/src/lib/game/objects/buildings/wagonStop.ts new file mode 100644 index 00000000..4a0cf9f9 --- /dev/null +++ b/src/lib/game/objects/buildings/wagonStop.ts @@ -0,0 +1,32 @@ +import { Sprite } from "pixi.js" +import type { IGameBuildingWagonStop } from "$lib/game/types" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Building } from "./building" + +interface IWagonStopOptions { + scene: GameScene + x: number + y: number +} + +export class WagonStop extends Building implements IGameBuildingWagonStop { + constructor({ scene, x, y }: IWagonStopOptions) { + super({ scene, x, y }) + + this.initGraphics() + } + + private initGraphics() { + const sprite = Sprite.from("wagonStop1") + if (sprite) { + sprite.anchor.set(0.5, 0.92) + this.addChild(sprite) + } + } + + animate() { + super.animate() + + this.zIndex = Math.round(this.y - 100) + } +} diff --git a/src/lib/game/objects/buildings/warehouse.ts b/src/lib/game/objects/buildings/warehouse.ts new file mode 100644 index 00000000..ae05468e --- /dev/null +++ b/src/lib/game/objects/buildings/warehouse.ts @@ -0,0 +1,41 @@ +import { Sprite } from "pixi.js" +import type { IGameBuildingWarehouse } from "$lib/game/types" +import { BuildingInterface } from "../../components/buildingInterface" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Building } from "./building" + +interface IWarehouseOptions { + scene: GameScene + x: number + y: number +} + +export class Warehouse extends Building implements IGameBuildingWarehouse { + public interface!: BuildingInterface + + constructor({ scene, x, y }: IWarehouseOptions) { + super({ scene, x, y }) + + this.initGraphics() + // this.initInterface() + } + + private initGraphics() { + const sprite = Sprite.from("warehouse1") + if (sprite) { + sprite.anchor.set(0.5, 1) + this.addChild(sprite) + } + } + + private initInterface() { + this.interface = new BuildingInterface(this) + this.addChild(this.interface) + } + + animate() { + super.animate() + + //this.interface.animate() + } +} diff --git a/src/lib/game/objects/flag.ts b/src/lib/game/objects/flag.ts new file mode 100644 index 00000000..d5ae99ab --- /dev/null +++ b/src/lib/game/objects/flag.ts @@ -0,0 +1,80 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectFlag } from "$lib/game/types" +import type { GameScene } from "../scenes/gameScene.ts" +import { GameObject } from "./gameObject.ts" + +interface IFlagOptions { + scene: GameScene + x: number + y: number + type: IGameObjectFlag["type"] + offsetX?: number + offsetY?: number +} + +export class Flag extends GameObject implements IGameObjectFlag { + public type!: IGameObjectFlag["type"] + + public isReserved: boolean + public offsetX: number + public offsetY: number + + constructor({ scene, x, y, type, offsetX, offsetY }: IFlagOptions) { + super({ scene, x, y }) + + this.type = type + this.isReserved = false + this.offsetX = offsetX ?? 0 + this.offsetY = offsetY ?? 0 + + this.visible = false + + this.initGraphics() + } + + public live() { + if (this.target?.state === "DESTROYED") { + this.removeTarget() + } + } + + private initGraphics() { + const sprite = this.getSpriteByType() + if (sprite) { + sprite.anchor.set(0.5, 1) + this.addChild(sprite) + } + } + + getSpriteByType() { + if ( + this.type === "MOVEMENT" || + this.type === "WAGON_NEAR_MOVEMENT" || + this.type === "WAGON_MOVEMENT" + ) { + return Sprite.from("flag1") + } + if (this.type === "RESOURCE") { + return Sprite.from("flag2") + } + } + + public animate() { + if (this.scene.game.checkIfThisFlagIsTarget(this.id)) { + this.visible = true + return + } + + if (this.type === "WAGON_MOVEMENT") { + this.visible = true + return + } + + if (this.state === "DESTROYED") { + this.visible = false + return + } + + this.visible = false + } +} diff --git a/src/lib/game/objects/gameObject.ts b/src/lib/game/objects/gameObject.ts new file mode 100644 index 00000000..fbcb2de0 --- /dev/null +++ b/src/lib/game/objects/gameObject.ts @@ -0,0 +1,141 @@ +import { createId } from "@paralleldrive/cuid2" +import { Container } from "pixi.js" +import type { IGameObject, IGameScript, } from "$lib/game/types" +import type { GameScene } from "../scenes/gameScene.ts" + +interface IGameObjectOptions { + scene: GameScene + id?: string + x?: number + y?: number +} + +export class GameObject extends Container implements IGameObject { + public id: string + public state!: IGameObject["state"] + public direction: IGameObject["direction"] + public entity: IGameObject["entity"] + public target: IGameObject["target"] + public health!: IGameObject["health"] + public speedPerSecond!: IGameObject["speedPerSecond"] + public size!: IGameObject["size"] + + public scene: GameScene + public script: IGameScript | undefined + public minDistance = 1 + public isOnWagonPath = false + + constructor({ scene, x, y, id }: IGameObjectOptions) { + super() + + this.scene = scene + + this.id = id ?? createId() + this.x = x ?? 0 + this.y = y ?? 0 + this.entity = "TREE" + this.direction = "RIGHT" + + this.scene.game.addChild(this) + } + + animate(): void { + this.zIndex = Math.round(this.y) + } + + live(): void { + } + + public move() { + const isOnTarget = this.checkIfIsOnTarget() + if (isOnTarget) { + this.stop() + return false + } + + if (!this.target || !this.target.x || !this.target.y) { + this.stop() + return false + } + + const distanceToX = this.getDistanceToTargetX() + const distanceToY = this.getDistanceToTargetY() + + // Fix diagonal speed + const speed = this.speedPerSecond / this.scene.game.tick + const finalSpeed = distanceToX > 0 && distanceToY > 0 ? speed * 0.75 : speed + + this.moveX(finalSpeed > distanceToX ? distanceToX : finalSpeed) + this.moveY(finalSpeed > distanceToY ? distanceToY : finalSpeed) + return true + } + + moveX(speed: number) { + if (!this.target?.x || this.target.x === this.x) { + return + } + + if (this.x < this.target.x) { + this.direction = "RIGHT" + this.x += speed + } + if (this.x > this.target.x) { + this.x -= speed + this.direction = "LEFT" + } + } + + moveY(speed: number) { + if (!this.target?.y || this.target.y === this.y) { + return + } + + if (this.y < this.target.y) { + this.y += speed + } + if (this.y > this.target.y) { + this.y -= speed + } + } + + stop() { + this.state = "IDLE" + } + + public destroy() { + super.destroy() + this.size = 0 + this.health = 0 + this.state = "DESTROYED" + } + + checkIfIsOnTarget() { + return ( + this.getDistanceToTargetX() + this.getDistanceToTargetY() <= + this.minDistance + ) + } + + getDistanceToTargetX() { + if (!this.target?.x) { + return 0 + } + return Math.abs(this.target.x - this.x) + } + + getDistanceToTargetY() { + if (!this.target?.y) { + return 0 + } + return Math.abs(this.target.y - this.y) + } + + public setTarget(target: IGameObject) { + this.target = target + this.state = "MOVING" + } + + public removeTarget() { + this.target = undefined + } +} diff --git a/src/lib/game/objects/index.ts b/src/lib/game/objects/index.ts new file mode 100644 index 00000000..8fb10428 --- /dev/null +++ b/src/lib/game/objects/index.ts @@ -0,0 +1,9 @@ +export { GameObject } from "./gameObject" +export { Rabbit } from "./rabbit" +export { Stone } from "./stone" +export { Tree } from "./tree" +export { Wolf } from "./wolf" +export { Flag } from "./flag" +export { Lake } from "./lake" +export { Area } from "./area" +export { Wagon } from "./wagon" diff --git a/src/lib/game/objects/lake.ts b/src/lib/game/objects/lake.ts new file mode 100644 index 00000000..5bc9a3c2 --- /dev/null +++ b/src/lib/game/objects/lake.ts @@ -0,0 +1,78 @@ +import type { IGameObjectLake } from "$lib/game/types" +import { AssetsManager } from "../utils" +import { GameObject } from "./gameObject.ts" +import { GameScene } from "../scenes/gameScene"; +import { Water } from "./water"; + +interface ILakeOptions { + scene: GameScene + x: number + y: number +} + +export class Lake extends GameObject implements IGameObjectLake { + public water: Water[] = [] + + constructor({ scene, x, y }: ILakeOptions) { + super({ scene, x, y }) + + this.generate(13) + this.initGraphics() + } + + public animate() { + super.animate(); + + this.zIndex = 0 + } + + generate(r: number) { + for (let y = r; y >= -r; --y) { + for (let x = -r; x <= r; x++) { + const value = x ** 2 + y ** 2 + + if (value < r ** 2) { + this.draw(x, y) + } + } + } + } + + draw(x: number, y: number) { + const water = new Water({ scene: this.scene, x: x * 32, y: y * 32 }) + this.water.push(water) + } + + init(width: number, height: number) { + const gridX = Math.ceil(width / 32) + const gridY = Math.floor(height / 32) + + console.log(gridX, gridY) + + //const center = { x: Math.round(width / 2), y: Math.round(height / 2) } + + for (let i = 0; i < gridX; i++) { + for (let j = 0; j < gridY; j++) { + const x = i * 32 + const y = j * 32 + + // if (x <= center.x && y <= center.y) { + // continue + // } + + const water = new Water({ scene: this.scene, x, y }) + this.water.push(water) + } + } + } + + private initGraphics() { + for (const w of this.water) { + const sprite = AssetsManager.getRandomSpriteForWater() + sprite.anchor.set(0.5, 1) + sprite.x = w.x + sprite.y = w.y + this.addChild(sprite) + } + } +} diff --git a/src/lib/game/objects/rabbit.ts b/src/lib/game/objects/rabbit.ts new file mode 100644 index 00000000..692ba835 --- /dev/null +++ b/src/lib/game/objects/rabbit.ts @@ -0,0 +1,59 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectRabbit } from "$lib/game/types" +import type { Game } from "../game" +import { GameObject } from "./gameObject.ts" + +interface IRabbitOptions { + game: Game + object: IGameObjectRabbit +} + +export class Rabbit extends GameObject implements IGameObjectRabbit { + public animationAngle = 0 + + constructor({ game, object }: IRabbitOptions) { + super({ game, ...object }) + + this.update(object) + this.init() + } + + init() { + const spriteRight = Sprite.from("rabbitRight") + const spriteLeft = Sprite.from("rabbitLeft") + + spriteRight.anchor.set(0.5, 1) + spriteLeft.anchor.set(0.5, 1) + + spriteLeft.direction = "LEFT" + spriteRight.direction = "RIGHT" + + this.addChild(spriteRight, spriteLeft) + } + + animate() { + // Hide all + for (const t of this.children) { + t.visible = false + } + + // Visible only 1 + const sprite = this.children.find((t) => t.direction === this.direction) + if (sprite) { + sprite.visible = true + } + + if (this.state === "MOVING") { + this.angle = this.animationAngle + this.shakeAnimation() + } + } + + shakeAnimation() { + if (this.animationAngle >= 4) { + this.animationAngle = 0 + return + } + this.animationAngle += 0.08 + } +} diff --git a/src/lib/game/objects/stone.ts b/src/lib/game/objects/stone.ts new file mode 100644 index 00000000..e9d41cf3 --- /dev/null +++ b/src/lib/game/objects/stone.ts @@ -0,0 +1,100 @@ +import { Sprite } from "pixi.js" +import { type IGameObjectStone } from "$lib/game/types" +import type { GameScene } from "../scenes/gameScene" +import { GameObject } from "./gameObject.ts" +import { getRandomInRange } from "$lib/random"; + +interface IStoneOptions { + scene: GameScene + x: number + y: number + resource?: number + size?: number + health?: number +} + +export class Stone extends GameObject implements IGameObjectStone { + public type!: IGameObjectStone["type"] + public resource!: number + + public isReserved = false + public animationAngle = 0 + public animationHighSpeed = 0.05 + + constructor({ scene, x, y, resource, size }: IStoneOptions) { + super({ scene, x, y }) + + this.state = "IDLE" + this.resource = resource ?? getRandomInRange(1, 5) + this.size = size ?? 100 + + this.initGraphics() + } + + private initGraphics() { + const sprite = this.getSpriteByType() + if (sprite) { + sprite.anchor.set(0.5, 1) + this.addChild(sprite) + } + } + + getSpriteByType() { + if (this.type === "1") { + return Sprite.from("stone1") + } + } + + public animate() { + super.animate() + + if (this.state === "DESTROYED") { + this.visible = false + } + + if (this.state === "MINING") { + this.scale = 0.98 + this.shakeAnimation() + } + } + + shakeAnimation() { + if (Math.abs(this.animationAngle) >= 0.5) { + this.animationHighSpeed *= -1 + } + this.animationAngle += this.animationHighSpeed + this.angle = this.animationAngle + } + + live() { + if (this.state === "MINING") { + if (this.health <= 0) { + this.setAsMined() + } + + const random = getRandomInRange(1, 20) + if (random <= 1 && this.health > 0) { + this.state = "IDLE" + this.isReserved = false + } + + return + } + + if (this.state === "DESTROYED") { + return + } + } + + mine() { + this.state = "MINING" + this.isReserved = true + this.health -= 0.08 + } + + setAsMined() { + this.size = 0 + this.health = 0 + this.state = "DESTROYED" + } +} diff --git a/src/lib/game/objects/tree.ts b/src/lib/game/objects/tree.ts new file mode 100644 index 00000000..1926e80f --- /dev/null +++ b/src/lib/game/objects/tree.ts @@ -0,0 +1,189 @@ +import { Sprite } from "pixi.js" +import { type IGameObjectTree } from "$lib/game/types" +import type { GameScene } from "../scenes/gameScene" +import { GameObject } from "./gameObject" +import { getRandomInRange } from "$lib/random"; + +interface ITreeOptions { + scene: GameScene + x: number + y: number + resource?: number + size?: number + health?: number + growSpeed?: number + type?: IGameObjectTree["type"] + variant?: IGameObjectTree["variant"] +} + +export class Tree extends GameObject implements IGameObjectTree { + public type!: IGameObjectTree["type"] + public variant!: IGameObjectTree["variant"] + public resource!: number + public isReadyToChop!: boolean + + public isReserved = false + private minSizeToChop = 75 + private maxSize = 100 + private growSpeedPerSecond = 0.5 + private animationAngle = getRandomInRange(-1, 1) + private animationSlowSpeed = 0.04 + private animationHighSpeed = 0.15 + + constructor({ + scene, + x, + y, + resource, + size, + health, + type, + variant, + }: ITreeOptions) { + super({ scene, x, y }) + + this.state = "IDLE" + this.resource = resource ?? getRandomInRange(1, 5) + this.size = size ?? 100 + this.health = health ?? 100 + this.type = type ?? this.getNewType() + this.variant = variant ?? this.getNewVariant() + + this.initGraphics() + } + + public live() { + this.checkHealth() + + switch (this.state) { + case "IDLE": + this.grow() + break + case "CHOPPING": + this.handleChoppingState() + break + case "DESTROYED": + break + } + } + + public animate() { + super.animate() + + this.scale = this.size / 100 + + if (this.state === "IDLE") { + this.shakeOnWind() + } + + if (this.state === "DESTROYED") { + this.visible = false + } + + if (this.state === "CHOPPING") { + this.shakeAnimation() + } + } + + public chop() { + this.state = "CHOPPING" + this.isReserved = true + this.health -= 0.08 + } + + private initGraphics() { + const sprite = this.getSpriteByType() + if (sprite) { + sprite.anchor.set(0.5, 1) + + // Some random to flip horizontally + const random = getRandomInRange(1, 2) + if (random === 1) { + sprite.scale.x = -1 + } + + this.addChild(sprite) + } + } + + private getSpriteByType() { + if (this.variant === "GREEN") { + return Sprite.from(`tree${this.type}Green`) + } + if (this.variant === "BLUE") { + return Sprite.from(`tree${this.type}Blue`) + } + if (this.variant === "STONE") { + return Sprite.from(`tree${this.type}Stone`) + } + if (this.variant === "TEAL") { + return Sprite.from(`tree${this.type}Teal`) + } + if (this.variant === "TOXIC") { + return Sprite.from(`tree${this.type}Toxic`) + } + if (this.variant === "VIOLET") { + return Sprite.from(`tree${this.type}Violet`) + } + } + + private shakeAnimation() { + if (Math.abs(this.animationAngle) >= 2.5) { + this.animationHighSpeed *= -1 + } + this.animationAngle += this.animationHighSpeed + this.angle = this.animationAngle + } + + private shakeOnWind() { + if (Math.abs(this.animationAngle) >= 1.5) { + this.animationSlowSpeed *= -1 + } + this.animationAngle += this.animationSlowSpeed + this.angle = this.animationAngle + } + + private checkHealth() { + if (this.health <= 0) { + this.destroy() + } + } + + private handleChoppingState() { + const random = getRandomInRange(1, 20) + if (random <= 1) { + this.state = "IDLE" + this.isReserved = false + } + } + + private grow() { + if (this.size >= this.minSizeToChop && !this.isReadyToChop) { + this.isReadyToChop = true + } + if (this.size >= this.maxSize) { + return + } + + this.size += this.growSpeedPerSecond / this.scene.game.tick + } + + private getNewType(): IGameObjectTree["type"] { + const types: IGameObjectTree["type"][] = ["1", "2", "3", "4", "5"] + const index = getRandomInRange(0, types.length - 1) + return types[index] + } + + private getNewVariant(): IGameObjectTree["variant"] { + const variants: IGameObjectTree["variant"][] = [ + "GREEN", + "BLUE", + "STONE", + "TEAL", + "TOXIC", + "VIOLET", + ] + const index = getRandomInRange(0, variants.length - 1) + return variants[index] + } +} diff --git a/src/lib/game/objects/units/courier.ts b/src/lib/game/objects/units/courier.ts new file mode 100644 index 00000000..eabc8a6e --- /dev/null +++ b/src/lib/game/objects/units/courier.ts @@ -0,0 +1,31 @@ +import type { IGameObjectCourier } from "$lib/game/types" +import { generateUnitUserName } from "../../common/generators/unitName.ts" +import { generateUnitTop } from "../../common/generators/unitTop.ts" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Unit } from "./unit" + +interface ICourierOptions { + scene: GameScene + x: number + y: number +} + +export class Courier extends Unit implements IGameObjectCourier { + constructor({ scene, x, y }: ICourierOptions) { + super({ + scene, + x, + y, + }) + + this.speedPerSecond = 100 + this.minDistance = 15 + this.userName = generateUnitUserName() + + this.initVisual({ + head: "1", + hairstyle: "BOLD", + top: generateUnitTop(), + }) + } +} diff --git a/src/lib/game/objects/units/farmer.ts b/src/lib/game/objects/units/farmer.ts new file mode 100644 index 00000000..decf35f0 --- /dev/null +++ b/src/lib/game/objects/units/farmer.ts @@ -0,0 +1,31 @@ +import type { IGameObjectFarmer } from "$lib/game/types" +import { generateUnitUserName } from "../../common/generators/unitName.ts" +import { generateUnitTop } from "../../common/generators/unitTop.ts" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Unit } from "./unit" + +interface IFarmerOptions { + scene: GameScene + x: number + y: number +} + +export class Farmer extends Unit implements IGameObjectFarmer { + constructor({ scene, x, y }: IFarmerOptions) { + super({ + scene, + x, + y, + }) + + this.speedPerSecond = 70 + this.minDistance = 10 + this.userName = generateUnitUserName() + + this.initVisual({ + head: "1", + hairstyle: "ORANGE_WITH_BEARD", + top: generateUnitTop(), + }) + } +} diff --git a/src/lib/game/objects/units/index.ts b/src/lib/game/objects/units/index.ts new file mode 100644 index 00000000..26dc7bb6 --- /dev/null +++ b/src/lib/game/objects/units/index.ts @@ -0,0 +1,7 @@ +export { Courier } from "./courier" +export { Farmer } from "./farmer" +export { Mechanic } from "./mechanic" +export { Player } from "./player" +export { Raider } from "./raider" +export { Trader } from "./trader" +export { Unit } from "./unit" diff --git a/src/lib/game/objects/units/mechanic.ts b/src/lib/game/objects/units/mechanic.ts new file mode 100644 index 00000000..3476a70f --- /dev/null +++ b/src/lib/game/objects/units/mechanic.ts @@ -0,0 +1,33 @@ +import type { IGameObjectMechanic } from "$lib/game/types" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Unit } from "./unit" + +interface IMechanicOptions { + scene: GameScene + x: number + y: number +} + +export class Mechanic extends Unit implements IGameObjectMechanic { + constructor({ scene, x, y }: IMechanicOptions) { + super({ + scene, + x, + y, + }) + + this.userName = "Mechanic" + + this.initVisual({ + head: "1", + hairstyle: "COAL_LONG", + top: "DARK_SILVER_SHIRT", + }) + } + + public animate() { + super.animate() + + this.zIndex = Math.round(this.y + 100) + } +} diff --git a/src/lib/game/objects/units/player.ts b/src/lib/game/objects/units/player.ts new file mode 100644 index 00000000..7d626566 --- /dev/null +++ b/src/lib/game/objects/units/player.ts @@ -0,0 +1,187 @@ +import { type IGameObjectPlayer, type IGameSkill } from "$lib/game/types" +import { Inventory, Skill } from "../../common" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Unit } from "./unit" +import { getRandomInRange } from "$lib/random"; + +interface IPlayerOptions { + scene: GameScene + id?: string + x: number + y: number +} + +export class Player extends Unit implements IGameObjectPlayer { + reputation!: number + villainPoints!: number + refuellerPoints!: number + raiderPoints!: number + skills!: Skill[] + lastActionAt!: IGameObjectPlayer["lastActionAt"] + + public inventoryId?: string + + constructor({ scene, id, x, y }: IPlayerOptions) { + super({ scene, id, x, y }) + + this.speedPerSecond = 2 + void this.initFromDB() + } + + async initFromDB() { + await this.readFromDB() + await this.initSkillsFromDB() + super.initVisual({ + head: "1", + hairstyle: "CLASSIC", + top: "VIOLET_SHIRT", + }) + } + + async chopTree() { + super.chopTree() + + await this.findOrCreateSkillInDB("WOODSMAN") + this.upSkill("WOODSMAN") + } + + async mineStone() { + super.mineStone() + + await this.findOrCreateSkillInDB("MINER") + this.upSkill("MINER") + } + + updateCoins(amount: number) { + this.coins = this.coins + amount + + // return db.player.update({ + // where: { id: this.id }, + // data: { + // coins: this.coins, + // }, + // }) + } + + addReputation(amount: number) { + this.reputation += amount + + // return db.player.update({ + // where: { id: this.id }, + // data: { + // reputation: this.reputation, + // }, + // }) + } + + addRefuellerPoints(amount: number) { + if (amount < 0) { + return + } + + this.refuellerPoints += amount + + // return db.player.update({ + // where: { id: this.id }, + // data: { + // refuellerPoints: this.refuellerPoints, + // }, + // }) + } + + addVillainPoints(amount: number) { + this.villainPoints += amount + + // return db.player.update({ + // where: { id: this.id }, + // data: { + // villainPoints: { + // increment: amount, + // }, + // }, + // }) + } + + addRaiderPoints(amount: number) { + this.raiderPoints += amount + + // return db.player.update({ + // where: { id: this.id }, + // data: { + // raiderPoints: { + // increment: amount, + // }, + // }, + // }) + } + + public async readFromDB() { + // const player = await db.player.findUnique({ where: { id: this.id } }) + // if (!player) { + // return + // } + // + // this.userName = player.userName + // this.coins = player.coins + // this.reputation = player.reputation + // this.villainPoints = player.villainPoints + // this.refuellerPoints = player.refuellerPoints + // this.raiderPoints = player.raiderPoints + // this.inventoryId = player.inventoryId + } + + public updateLastActionAt() { + this.lastActionAt = new Date() + // return db.player.update({ + // where: { id: this.id }, + // data: { + // lastActionAt: new Date(), + // }, + // }) + } + + public async initInventoryFromDB() { + if (!this.inventoryId) { + return + } + + const inventory = new Inventory({ + objectId: this.id, + id: this.inventoryId, + saveInDb: true, + }) + await inventory.init() + this.inventory = inventory + } + + public async initSkillsFromDB() { + this.skills = [] + const skills = await Skill.findAllInDB(this.id) + for (const skill of skills) { + const instance = new Skill({ id: skill.id }) + await instance.init() + this.skills.push(instance) + } + } + + async findOrCreateSkillInDB(type: IGameSkill["type"]) { + const skill = this.skills.find((skill) => skill.type === type) + if (!skill) { + await Skill.createInDB(this.id, type) + await this.initSkillsFromDB() + return this.skills.find((skill) => skill.type === type) as Skill + } + + return skill + } + + public upSkill(type: IGameSkill["type"]) { + const random = getRandomInRange(1, 200) + if (random <= 1) { + const skill = this.skills.find((skill) => skill.type === type) + if (skill) { + void skill.addXp() + } + } + } +} diff --git a/src/lib/game/objects/units/raider.ts b/src/lib/game/objects/units/raider.ts new file mode 100644 index 00000000..577da6df --- /dev/null +++ b/src/lib/game/objects/units/raider.ts @@ -0,0 +1,28 @@ +import type { IGameObjectRaider } from "$lib/game/types" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Unit } from "./unit" + +interface IRaiderOptions { + scene: GameScene + x: number + y: number +} + +export class Raider extends Unit implements IGameObjectRaider { + constructor({ scene, x, y }: IRaiderOptions) { + super({ + scene, + x, + y, + }) + + this.speedPerSecond = 1.5 + this.userName = "Raider" + + this.initVisual({ + head: "1", + hairstyle: "BOLD", + top: "BLACK_SHIRT", + }) + } +} diff --git a/src/lib/game/objects/units/trader.ts b/src/lib/game/objects/units/trader.ts new file mode 100644 index 00000000..a54e20d4 --- /dev/null +++ b/src/lib/game/objects/units/trader.ts @@ -0,0 +1,31 @@ +import type { IGameObjectTrader } from "$lib/game/types" +import { generateUnitUserName } from "../../common/generators/unitName.ts" +import { generateUnitTop } from "../../common/generators/unitTop.ts" +import type { GameScene } from "../../scenes/gameScene.ts" +import { Unit } from "./unit" + +interface ITraderOptions { + scene: GameScene + x: number + y: number +} + +export class Trader extends Unit implements IGameObjectTrader { + constructor({ scene, x, y }: ITraderOptions) { + super({ + scene, + x, + y, + }) + + this.speedPerSecond = 60 + this.minDistance = 5 + this.userName = generateUnitUserName() + + this.initVisual({ + head: "1", + hairstyle: "COAL_LONG", + top: generateUnitTop(), + }) + } +} diff --git a/src/lib/game/objects/units/unit.ts b/src/lib/game/objects/units/unit.ts new file mode 100644 index 00000000..11ecee3e --- /dev/null +++ b/src/lib/game/objects/units/unit.ts @@ -0,0 +1,294 @@ +import { createId } from "@paralleldrive/cuid2" +import type { AnimatedSprite } from "pixi.js" +import { type IGameObjectUnit } from "$lib/game/types" +import { Inventory } from "../../common" +import { DialogueInterface } from "../../components/dialogueInterface" +import type { GraphicsContainer } from "../../components/graphicsContainer" +import { UnitHairContainer } from "../../components/unitHairContainer" +import { UnitHeadContainer } from "../../components/unitHeadContainer" +import { UnitInterface } from "../../components/unitInterface" +import { UnitTopContainer } from "../../components/unitTopContainer" +import type { GameScene } from "../../scenes/gameScene.ts" +import { AssetsManager } from "../../utils" +import { Flag } from "../flag" +import { GameObject } from "../gameObject.ts" +import { Stone } from "../stone.ts" +import { Tree } from "../tree.ts" +import { getRandomInRange } from "$lib/random"; + +interface IUnitOptions { + scene: GameScene + id?: string + x: number + y: number +} + +export class Unit extends GameObject implements IGameObjectUnit { + public inventory!: Inventory + public visual!: IGameObjectUnit["visual"] + public userName!: IGameObjectUnit["userName"] + public coins = 0 + public dialogue!: IGameObjectUnit["dialogue"] + + private interface!: UnitInterface + private dialogueInterface!: DialogueInterface + children: GraphicsContainer[] = [] + private readonly animationMovingLeft!: AnimatedSprite + private readonly animationMovingRight!: AnimatedSprite + + constructor({ scene, x, y, id }: IUnitOptions) { + super({ scene, x, y, id }) + + this.initInventory() + this.initVisual() + this.initDialogue() + this.coins = 0 + this.state = "IDLE" + + this.animationMovingRight = AssetsManager.getAnimatedSpriteHero("RIGHT") + this.animationMovingLeft = AssetsManager.getAnimatedSpriteHero("LEFT") + + this.initGraphics() + } + + public live() { + this.handleMessages() + + if (this.script) { + return this.script.live() + } + } + + private initInventory() { + this.inventory = new Inventory({ + objectId: this.id, + id: createId(), + saveInDb: false, + }) + } + + public initVisual(visual?: IGameObjectUnit["visual"]) { + this.visual = visual ?? { + head: "1", + hairstyle: "CLASSIC", + top: "VIOLET_SHIRT", + } + } + + private initDialogue() { + this.dialogue = { + messages: [], + } + } + + public addMessage(message: string) { + const MAX_CHARS = 100 + const messagePrepared = + message.trim().slice(0, MAX_CHARS) + + (message.length > MAX_CHARS ? "..." : "") + + this.dialogue.messages.push({ + id: createId(), + text: messagePrepared, + }) + } + + public handleMessages() { + const random = getRandomInRange(1, 200) + if (random === 1) { + this.dialogue.messages.splice(0, 1) + } + } + + public chopTree() { + if (this.target instanceof Tree && this.target.state !== "DESTROYED") { + this.direction = "RIGHT" + this.state = "CHOPPING" + this.checkAndBreakTool("AXE") + + this.target.chop() + } + } + + public mineStone() { + if (this.target instanceof Stone && this.target.state !== "DESTROYED") { + this.direction = "RIGHT" + this.state = "MINING" + this.checkAndBreakTool("PICKAXE") + + this.target.mine() + } + } + + checkAndBreakTool(type: "AXE" | "PICKAXE") { + const tool = this.inventory.items.find((item) => item.type === type) + if (tool) { + //this.target.health -= 0.16 + const random = getRandomInRange(1, 40) + if (random <= 1) { + void this.inventory.checkAndBreakItem(tool, 1) + } + } + } + + private initGraphics() { + const top = this.initTop() + const head = this.initHead() + const hair = this.initHair() + + this.initInterface() + + this.addChild( + ...top, + ...head, + ...hair, + this.animationMovingLeft, + this.animationMovingRight, + this.interface, + this.dialogueInterface, + ) + } + + initTop() { + return UnitTopContainer.getAll() + } + + initHead() { + return UnitHeadContainer.getAll() + } + + initHair() { + return UnitHairContainer.getAll() + } + + initInterface() { + this.interface = new UnitInterface(this) + this.dialogueInterface = new DialogueInterface(this) + } + + public animate() { + super.animate() + + this.zIndex = Math.round(this.y + 1) + + for (const container of this.children) { + container.visible = false + + if (this.state === "MOVING") { + this.animationMovingLeft.animationSpeed = 0.25 + this.animationMovingRight.animationSpeed = 0.25 + + if (this.direction === "RIGHT") { + this.animationMovingRight.visible = true + this.animationMovingRight.play() + } + if (this.direction === "LEFT") { + this.animationMovingLeft.visible = true + this.animationMovingLeft.play() + } + } + + if ( + this.state === "IDLE" || + this.state === "CHOPPING" || + this.state === "MINING" + ) { + this.animationMovingLeft.animationSpeed = 0 + this.animationMovingRight.animationSpeed = 0 + this.animationMovingLeft.currentFrame = 0 + this.animationMovingRight.currentFrame = 0 + + if (this.direction === "LEFT") { + this.animationMovingLeft.visible = true + } + if (this.direction === "RIGHT") { + this.animationMovingRight.visible = true + } + } + + this.drawTop(container) + this.drawHead(container) + this.drawHair(container) + } + + // this.interface.animate() + // this.dialogueInterface.animate() + + this.showToolInHand() + this.handleSoundByState() + + if (this.target && this.target instanceof Flag) { + this.target.visible = true + } + } + + drawTop(container: GraphicsContainer) { + if (container instanceof UnitTopContainer) { + if (container.visual !== this.visual.top) { + return + } + if (container.direction !== this.direction) { + return + } + + container.visible = true + } + } + + drawHead(container: GraphicsContainer) { + if (container instanceof UnitHeadContainer) { + if (container.visual !== this.visual.head) { + return + } + if (container.direction !== this.direction) { + return + } + + container.visible = true + } + } + + drawHair(container: GraphicsContainer) { + if (container instanceof UnitHairContainer) { + if (container.visual !== this.visual.hairstyle) { + return + } + if (container.direction !== this.direction) { + return + } + + container.visible = true + } + } + + showToolInHand() { + if (this.state === "CHOPPING") { + this.interface.showAxeInHand() + } + if (this.state === "MINING") { + this.interface.showPickaxeInHand() + } + } + + handleSoundByState() { + if (this.state === "CHOPPING") { + if (this.inventory?.items.find((item) => item.type === "AXE")) { + this.scene.game.audio.playSound("CHOP_HIT") + return + } + + this.scene.game.audio.playSound("HAND_HIT") + return + } + + if (this.state === "MINING") { + if (this.inventory?.items.find((item) => item.type === "PICKAXE")) { + this.scene.game.audio.playSound("MINE_HIT") + return + } + + this.scene.game.audio.playSound("HAND_HIT") + return + } + } +} diff --git a/src/lib/game/objects/wagon.ts b/src/lib/game/objects/wagon.ts new file mode 100644 index 00000000..32419cd5 --- /dev/null +++ b/src/lib/game/objects/wagon.ts @@ -0,0 +1,295 @@ +import { createId } from "@paralleldrive/cuid2" +import { Sprite } from "pixi.js" +import type { IGameObjectWagon } from "$lib/game/types" +import { Inventory } from "../common" +import type { GraphicsContainer } from "../components/graphicsContainer" +import { WagonCargoContainer } from "../components/wagonCargoContainer" +import { + WagonEngineCloudsContainer +} from "../components/wagonEngineCloudsContainer" +import { WagonEngineContainer } from "../components/wagonEngineContainer" +import { WagonFuelBoxContainer } from "../components/wagonFuelBoxContainer" +import { WagonWheelContainer } from "../components/wagonWheelContainer" +import type { GameScene } from "../scenes/gameScene.ts" +import { GameObject } from "./gameObject.ts" +import { Mechanic } from "./units" + +interface IWagonOptions { + scene: GameScene + x: number + y: number +} + +export class Wagon extends GameObject implements IGameObjectWagon { + public fuel!: number + public visibilityArea!: IGameObjectWagon["visibilityArea"] + public cargoType: IGameObjectWagon["cargoType"] + + public children: GraphicsContainer[] = [] + public cargo: Inventory | undefined + public mechanic!: Mechanic + public serverDataArea!: IGameObjectWagon["visibilityArea"] + public collisionArea!: IGameObjectWagon["visibilityArea"] + + constructor({ scene, x, y }: IWagonOptions) { + super({ scene, x, y }) + + this.state = "IDLE" + this.speedPerSecond = 0 + this.fuel = 2000 + this.updateVisibilityArea() + this.updateServerDataArea() + + this.initMechanic() + this.initGraphics() + } + + public live() { + this.updateVisibilityArea() + this.updateServerDataArea() + this.updateCollisionArea() + this.updateMechanic() + this.consumeFuel() + + if (this.state === "IDLE") { + return + } + if (this.state === "WAITING") { + return + } + } + + consumeFuel() { + if (this.speedPerSecond <= 0) { + return + } + + this.fuel -= this.speedPerSecond * 2 + } + + refuel(woodAmount: number) { + if (woodAmount < 0) { + return + } + + this.fuel += woodAmount * 5 * 40 + } + + emptyFuel() { + this.fuel = 0 + } + + updateVisibilityArea() { + const offsetX = 2560 / 2 + const offsetY = 1440 / 2 + + this.visibilityArea = { + startX: this.x - offsetX, + endX: this.x + offsetX, + startY: this.y - offsetY, + endY: this.y + offsetY, + } + } + + updateServerDataArea() { + const offsetX = 2560 * 1.5 + const offsetY = 1440 + + this.serverDataArea = { + startX: this.x - offsetX, + endX: this.x + offsetX, + startY: this.y - offsetY, + endY: this.y + offsetY, + } + } + + updateCollisionArea() { + const offsetX = 250 + const offsetY = 180 + + this.collisionArea = { + startX: this.x - offsetX, + endX: this.x + offsetX, + startY: this.y - offsetY, + endY: this.y + offsetY, + } + } + + public checkIfPointInCollisionArea(point: { x: number; y: number }) { + return ( + this.collisionArea.startX < point.x && + point.x < this.collisionArea.endX && + this.collisionArea.startY < point.y && + point.y < this.collisionArea.endY + ) + } + + public checkIfPointInVisibilityArea(point: { x: number; y: number }) { + return ( + this.visibilityArea.startX < point.x && + point.x < this.visibilityArea.endX && + this.visibilityArea.startY < point.y && + point.y < this.visibilityArea.endY + ) + } + + public checkIfPointInServerDataArea(point: { x: number; y: number }) { + return ( + this.serverDataArea.startX < point.x && + point.x < this.serverDataArea.endX && + this.serverDataArea.startY < point.y && + point.y < this.serverDataArea.endY + ) + } + + initMechanic() { + this.mechanic = new Mechanic({ + scene: this.scene, + x: this.x, + y: this.y, + }) + } + + updateMechanic() { + this.mechanic.live() + this.mechanic.direction = "LEFT" + this.mechanic.x = this.x - 50 + this.mechanic.y = this.y - 48 + } + + public setCargo() { + this.cargo = new Inventory({ + id: createId(), + saveInDb: false, + objectId: this.id, + }) + void this.cargo.addOrCreateItem("WOOD", 100) + this.cargoType = "CHEST" + } + + public emptyCargo() { + this.cargo = undefined + this.cargoType = undefined + } + + private initGraphics() { + const spriteSide = Sprite.from("wagonBase1") + spriteSide.anchor.set(0.5, 1) + spriteSide.scale = 0.75 + + const spriteBase = Sprite.from("wagonBase2") + spriteBase.anchor.set(0.5, 1) + spriteBase.scale = 0.75 + + const cargo = WagonCargoContainer.create() + cargo.scale = 0.75 + + const engine = WagonEngineContainer.create("wagonEngine1", "RIGHT") + engine.scale = 0.75 + + const storage = WagonFuelBoxContainer.create() + storage.scale = 0.75 + + const wheel1 = WagonWheelContainer.create("wagonWheel1", "RIGHT", "LEFT") + const wheel2 = WagonWheelContainer.create("wagonWheel1", "RIGHT", "RIGHT") + wheel1.scale = 0.75 + wheel2.scale = 0.75 + + const clouds = new WagonEngineCloudsContainer() + + this.addChild( + spriteBase, + engine, + cargo, + spriteSide, + storage, + wheel1, + wheel2, + clouds, + ) + } + + public animate() { + super.animate() + + for (const container of this.children) { + container.visible = true + + this.drawWheels(container) + this.drawEngine(container) + this.drawCargo(container) + this.drawFuel(container) + + if (container instanceof WagonEngineCloudsContainer) { + container.animate(this.speedPerSecond) + } + } + + this.handleSoundByState() + } + + drawWheels(container: GraphicsContainer) { + if (container instanceof WagonWheelContainer) { + if (container.side === "LEFT") { + container.x = -123 + container.y = -16 + } + if (container.side === "RIGHT") { + container.x = 123 + container.y = -16 + } + + container.visible = true + + const wheelRotation = this.direction === "LEFT" ? -1 : 1 + + container.angle += (wheelRotation * this.speedPerSecond) / 2.5 + } + } + + drawEngine(container: GraphicsContainer) { + if (container instanceof WagonEngineContainer) { + container.x = -102 + container.y = -58 + + container.visible = true + } + } + + drawCargo(container: GraphicsContainer) { + if (container instanceof WagonCargoContainer) { + if (this.cargoType === "CHEST") { + container.visible = true + for (const c of container.children) { + c.visible = true + } + } + + if (!this.cargoType) { + container.visible = false + } + } + } + + drawFuel(container: GraphicsContainer) { + let initFuel = this.fuel + if (container instanceof WagonFuelBoxContainer) { + for (const c of container.children) { + for (const fuelSprite of c.children) { + fuelSprite.visible = false + initFuel -= 500 + + if (initFuel > 500) { + fuelSprite.visible = true + } + } + } + } + } + + handleSoundByState() { + if (this.state === "MOVING") { + this.scene.game.audio.playSound("WAGON_MOVING") + } + } +} diff --git a/src/lib/game/objects/water.ts b/src/lib/game/objects/water.ts new file mode 100644 index 00000000..c1d024d7 --- /dev/null +++ b/src/lib/game/objects/water.ts @@ -0,0 +1,15 @@ +import type { IGameObjectWater } from "$lib/game/types" +import { GameObject } from "./gameObject" +import { GameScene } from "../scenes/gameScene"; + +interface IWaterOptions { + scene: GameScene + x: number + y: number +} + +export class Water extends GameObject implements IGameObjectWater { + constructor({ scene, x, y }: IWaterOptions) { + super({ scene, x, y }) + } +} diff --git a/src/lib/game/objects/wolf.ts b/src/lib/game/objects/wolf.ts new file mode 100644 index 00000000..eee21657 --- /dev/null +++ b/src/lib/game/objects/wolf.ts @@ -0,0 +1,60 @@ +import { Sprite } from "pixi.js" +import type { IGameObjectWolf } from "$lib/game/types" +import type { Game } from "../game" +import { GameObject } from "./gameObject.ts" + +interface IWolfOptions { + game: Game + object: IGameObjectWolf +} + +export class Wolf extends GameObject implements IGameObjectWolf { + public animationAngle = 0 + public animationSlowSpeed = 0.1 + + constructor({ game, object }: IWolfOptions) { + super({ game, ...object }) + + this.update(object) + this.init() + } + + init() { + const spriteRight = Sprite.from("wolfRight") + const spriteLeft = Sprite.from("wolfLeft") + + spriteRight.anchor.set(0.5, 1) + spriteLeft.anchor.set(0.5, 1) + + spriteRight.direction = "RIGHT" + spriteLeft.direction = "LEFT" + + this.addChild(spriteRight, spriteLeft) + } + + animate() { + // Hide all + for (const t of this.children) { + t.visible = false + } + + // Visible only 1 + const sprite = this.children.find((t) => t.direction === this.direction) + if (sprite) { + sprite.visible = true + } + + if (this.state === "MOVING") { + this.angle = this.animationAngle + this.shakeAnimation() + } + } + + shakeAnimation() { + if (Math.abs(this.animationAngle) >= 2) { + this.animationSlowSpeed *= -1 + } + this.animationAngle += this.animationSlowSpeed + this.angle = this.animationAngle + } +} diff --git a/src/lib/game/quests/noTradingPostQuest.ts b/src/lib/game/quests/noTradingPostQuest.ts new file mode 100644 index 00000000..f34b0d32 --- /dev/null +++ b/src/lib/game/quests/noTradingPostQuest.ts @@ -0,0 +1,61 @@ +import { createId } from "@paralleldrive/cuid2" +import type { IGameQuestTaskFunc } from "$lib/game/types" +import type { + DonateWoodToVillageAction +} from "../actions/donateWoodToVillageAction" +import { Quest } from "./quest" + +interface INoTradingPostQuestOptions { + creatorId: string + taskUpdateFunc1: IGameQuestTaskFunc + taskUpdateFunc2: IGameQuestTaskFunc + taskAction1: DonateWoodToVillageAction +} + +export class NoTradingPostQuest extends Quest { + constructor({ + creatorId, + taskUpdateFunc1, + taskUpdateFunc2, + taskAction1, + }: INoTradingPostQuestOptions) { + super({ + type: "SIDE", + title: "No Trading Post", + description: "The locals need help. Traders are expected to arrive.", + }) + + this.creatorId = creatorId + this.initTasks({ taskUpdateFunc1, taskUpdateFunc2, taskAction1 }) + } + + initTasks({ + taskUpdateFunc1, + taskUpdateFunc2, + taskAction1, + }: { + taskUpdateFunc1: IGameQuestTaskFunc + taskUpdateFunc2: IGameQuestTaskFunc + taskAction1: DonateWoodToVillageAction + }) { + this.tasks = [ + { + id: createId(), + status: "ACTIVE", + description: "Accumulate 25 wood in the warehouse", + progressNow: 0, + progressToSuccess: 25, + updateProgress: taskUpdateFunc1, + action: taskAction1, + }, + { + id: createId(), + status: "ACTIVE", + description: "Build Trading Post", + progressNow: false, + progressToSuccess: true, + updateProgress: taskUpdateFunc2, + }, + ] + } +} diff --git a/src/lib/game/quests/quest.ts b/src/lib/game/quests/quest.ts new file mode 100644 index 00000000..eae1844b --- /dev/null +++ b/src/lib/game/quests/quest.ts @@ -0,0 +1,30 @@ +import { createId } from "@paralleldrive/cuid2" +import type { IGameQuest } from "$lib/game/types" + +interface IQuestOptions { + type: IGameQuest["type"] + title: IGameQuest["title"] + description: IGameQuest["description"] +} + +export class Quest implements IGameQuest { + public id: string + public type: IGameQuest["type"] + public title: IGameQuest["title"] + public description: IGameQuest["description"] + public tasks: IGameQuest["tasks"] + public status: IGameQuest["status"] + public creatorId!: IGameQuest["creatorId"] + public conditions!: IGameQuest["conditions"] + + constructor({ type, title, description }: IQuestOptions) { + this.id = createId() + this.type = type + this.title = title + this.description = description + this.tasks = [] + + this.status = "ACTIVE" + this.conditions = {} + } +} diff --git a/src/lib/game/quests/treesAreRunningOutQuest.ts b/src/lib/game/quests/treesAreRunningOutQuest.ts new file mode 100644 index 00000000..f61a1ade --- /dev/null +++ b/src/lib/game/quests/treesAreRunningOutQuest.ts @@ -0,0 +1,48 @@ +import { createId } from "@paralleldrive/cuid2" +import type { IGameQuestTaskFunc } from "$lib/game/types" +import type { PlantTreeAction } from "../actions/plantTreeAction" +import { Quest } from "./quest" + +interface ITreesAreRunningOutQuestOptions { + creatorId: string + taskUpdateFunc1: IGameQuestTaskFunc + taskAction1: PlantTreeAction +} + +export class TreesAreRunningOutQuest extends Quest { + constructor({ + creatorId, + taskUpdateFunc1, + taskAction1, + }: ITreesAreRunningOutQuestOptions) { + super({ + type: "SIDE", + title: "The trees are running out!", + description: + "In the village, someone is actively cutting down trees. Help is needed!", + }) + + this.creatorId = creatorId + this.initTasks({ taskUpdateFunc1, taskAction1 }) + } + + initTasks({ + taskUpdateFunc1, + taskAction1, + }: { + taskUpdateFunc1: IGameQuestTaskFunc + taskAction1: PlantTreeAction + }) { + this.tasks = [ + { + id: createId(), + status: "ACTIVE", + description: "There should be at least 30 trees", + progressNow: 0, + progressToSuccess: 30, + updateProgress: taskUpdateFunc1, + action: taskAction1, + }, + ] + } +} diff --git a/src/lib/game/scenes/gameScene.ts b/src/lib/game/scenes/gameScene.ts new file mode 100644 index 00000000..8fcb40a8 --- /dev/null +++ b/src/lib/game/scenes/gameScene.ts @@ -0,0 +1,469 @@ +import { createId } from "@paralleldrive/cuid2" +import { + type GetSceneResponse, + type IGameChunk, + type IGameChunkTheme, + type IGameInventoryItem, +} from "$lib/game/types" +import { type GameChunk, Village } from "../chunks" +import { Group, Route } from "../common" +import type { Game } from "../game" +import { + Flag, + type GameObject, + type Rabbit, + Stone, + Tree, + type Wolf, +} from "../objects" +import { Player, Raider, Trader } from "../objects/units" +import { ChopTreeScript } from "../scripts/chopTreeScript" +import { + MoveOffScreenAndSelfDestroyScript +} from "../scripts/moveOffScreenAndSelfDestroyScript" +import { MoveToTargetScript } from "../scripts/moveToTargetScript" +import { ActionService } from "../services/actionService" +import { EventService } from "../services/eventService" +import { TradeService } from "../services/tradeService" +import { WagonService } from "../services/wagonService" +import { getRandomInRange } from "$lib/random"; +import { getDateMinusMinutes } from "$lib/date"; + +interface IGameSceneOptions { + game: Game +} + +export class GameScene { + public id: string + public game: Game + public objects: GameObject[] = [] + public group: Group + public chunks: GameChunk[] = [] + public chunkNow: GameChunk | undefined + + public actionService: ActionService + public eventService: EventService + public tradeService: TradeService + public wagonService: WagonService + + constructor({ game }: IGameSceneOptions) { + this.id = createId() + this.game = game + this.group = new Group() + + this.actionService = new ActionService({ scene: this }) + this.eventService = new EventService({ scene: this }) + this.tradeService = new TradeService({ scene: this }) + this.wagonService = new WagonService({ scene: this }) + } + + public live() { + this.eventService.update() + this.tradeService.update() + this.wagonService.update() + this.updateObjects() + this.updateChunks() + this.updateChunkNow() + } + + public destroy() { + this.objects = [] + } + + getChunkNow(): IGameChunk | null { + if (!this.chunkNow) { + return null + } + + return { + id: this.chunkNow.id, + title: this.chunkNow.title, + type: this.chunkNow.type, + center: this.chunkNow.center, + area: this.chunkNow.area, + } + } + + getWarehouseItems(): IGameInventoryItem[] | undefined { + if (this.chunkNow instanceof Village) { + const warehouse = this.chunkNow.getWarehouse() + if (warehouse) { + return warehouse.inventory.items + } + } + + return undefined + } + + getInfo(): GetSceneResponse { + return { + id: this.id, + commands: this.actionService.getAvailableCommands(), + events: this.eventService.getEvents(), + group: this.group.getGroup(), + wagon: this.wagonService.wagon, + chunk: this.getChunkNow(), + route: this.wagonService.routeService.getRoute(), + warehouseItems: this.getWarehouseItems(), + } + } + + updateObjects() { + this.removeInactivePlayers() + + for (const obj of this.objects) { + this.removeDestroyedObject(obj) + + if (obj instanceof Trader) { + this.tradeService.updateTrader(obj) + continue + } + if (obj instanceof Player) { + this.updatePlayer(obj) + continue + } + if (obj instanceof Raider) { + this.updateRaider(obj) + continue + } + + void obj.live() + } + } + + updateChunks() { + for (const chunk of this.chunks) { + for (const object of chunk.objects) { + if (object.state === "DESTROYED") { + chunk.removeObject(object) + } + } + + chunk.live() + } + } + + updateChunkNow() { + this.chunkNow = undefined + + for (const chunk of this.chunks) { + const isWagonOnThisChunk = chunk.checkIfPointIsInArea({ + x: this.wagonService.wagon.x, + y: this.wagonService.wagon.y, + }) + if (isWagonOnThisChunk) { + this.chunkNow = chunk + } + } + } + + updatePlayer(object: Player) { + object.live() + + if (object.script) { + return + } + + if (object.state === "IDLE") { + const random = getRandomInRange(1, 150) + if (random <= 1) { + const target = this.wagonService.findRandomNearFlag() + + object.script = new MoveToTargetScript({ + object, + target, + }) + } + } + } + + updateRabbit(object: Rabbit) { + object.live() + + if (object.state === "IDLE") { + const random = getRandomInRange(1, 100) + if (random <= 1) { + const randomObj = this.findRandomMovementFlag() + if (!randomObj) { + return + } + object.setTarget(randomObj) + } + } + } + + updateWolf(object: Wolf) { + object.live() + + if (object.state === "IDLE") { + const random = getRandomInRange(1, 100) + if (random <= 1) { + const randomObj = this.findRandomMovementFlag() + if (!randomObj) { + return + } + object.setTarget(randomObj) + } + } + } + + updateRaider(object: Raider) { + object.live() + + if (object.script) { + return + } + + // If there is an available tree + const availableTree = this.chunkNow?.getAvailableTree() + if (availableTree) { + const chopTreeFunc = (): boolean => { + object.chopTree() + if (!object.target || object.target.state === "DESTROYED") { + object.state = "IDLE" + if (object.target instanceof Tree) { + void object.inventory.addOrCreateItem( + "WOOD", + object.target?.resource, + ) + } + return true + } + return false + } + + object.script = new ChopTreeScript({ + object, + target: availableTree, + chopTreeFunc, + }) + + return + } + + if (object.state === "IDLE") { + const random = getRandomInRange(1, 100) + if (random <= 1) { + const randomObj = this.findRandomMovementFlag() + if (!randomObj) { + return + } + object.setTarget(randomObj) + } + } + } + + removeObject(object: GameObject) { + const index = this.objects.indexOf(object) + this.objects.splice(index, 1) + } + + async findOrCreatePlayer(id: string) { + const player = this.findPlayer(id) + if (!player && this.actionService.isActionPossible("CREATE_NEW_PLAYER")) { + return this.createPlayer(id) + } + return player + } + + public findPlayer(id: string) { + const player = this.objects.find((p) => p.id === id) + if (player instanceof Player) { + return player + } + } + + public findActivePlayers() { + return this.objects.filter((obj) => obj instanceof Player) as Player[] + } + + public removeInactivePlayers() { + const players = this.findActivePlayers() + for (const player of players) { + const checkTime = getDateMinusMinutes(8) + if (player.lastActionAt.getTime() <= checkTime.getTime()) { + if (player.script) { + continue + } + + const target = this.wagonService.findRandomOutFlag() + const selfDestroyFunc = () => { + this.group.remove(player) + this.removeObject(player) + } + + player.script = new MoveOffScreenAndSelfDestroyScript({ + target, + object: player, + selfDestroyFunc, + }) + } + } + } + + removeDestroyedObject(obj: GameObject) { + if (obj.state === "DESTROYED") { + this.removeObject(obj) + } + } + + async initPlayer(id: string) { + const instance = new Player({ scene: this, id, x: -100, y: -100 }) + await instance.initFromDB() + await instance.initInventoryFromDB() + + const flag = this.wagonService.findRandomOutFlag() + instance.x = flag.x + instance.y = flag.y + + return instance + } + + public async createPlayer(id: string): Promise { + const player = this.findPlayer(id) + if (!player) { + const instance = await this.initPlayer(id) + this.objects.push(instance) + return instance + } + return player + } + + getTreeToChop() { + // Part 1: Check trees on Wagon Path + const onlyOnPath = this.chunkNow?.objects.filter( + (obj) => + obj instanceof Tree && + obj.state !== "DESTROYED" && + !obj.isReserved && + obj.isOnWagonPath, + ) + if (onlyOnPath && onlyOnPath.length > 0) { + return this.determineNearestObject( + this.wagonService.wagon, + onlyOnPath, + ) as Tree + } + + // Part 2: Check nearest free tree + const other = this.chunkNow?.objects.filter( + (obj) => + obj instanceof Tree && + obj.state !== "DESTROYED" && + !obj.isReserved && + obj.isReadyToChop, + ) + if (other && other.length > 0) { + return this.determineNearestObject(this.wagonService.wagon, other) as Tree + } + } + + getStoneToMine() { + // Part 1: Check on Wagon Path + const onlyOnPath = this.chunkNow?.objects.filter( + (obj) => + obj instanceof Stone && + obj.state !== "DESTROYED" && + !obj.isReserved && + obj.isOnWagonPath, + ) + if (onlyOnPath && onlyOnPath.length > 0) { + return this.determineNearestObject( + this.wagonService.wagon, + onlyOnPath, + ) as Stone + } + + // Part 2: Check nearest free + const other = this.chunkNow?.objects.filter( + (obj) => + obj instanceof Stone && obj.state !== "DESTROYED" && !obj.isReserved, + ) + if (other && other.length > 0) { + return this.determineNearestObject( + this.wagonService.wagon, + other, + ) as Stone + } + } + + determineNearestObject( + point: { + x: number + y: number + }, + objects: GameObject[], + ) { + let closestObject = objects[0] + let shortestDistance = undefined + + for (const object of objects) { + const distance = Route.getDistanceBetween2Points(point, object) + if (!shortestDistance || distance < shortestDistance) { + shortestDistance = distance + closestObject = object + } + } + + return closestObject + } + + initRaiders(count: number) { + for (let i = 0; i < count; i++) { + const flag = this.wagonService.findRandomOutFlag() + + this.objects.push(new Raider({ scene: this, x: flag.x, y: flag.y })) + } + } + + public stopRaid() { + for (const object of this.objects) { + if (object instanceof Raider) { + const target = this.wagonService.findRandomOutFlag() + const selfDestroyFunc = () => { + this.removeObject(object) + } + + object.script = new MoveOffScreenAndSelfDestroyScript({ + target, + object, + selfDestroyFunc, + }) + } + } + } + + getRandomTheme(): IGameChunkTheme { + const themes: IGameChunkTheme[] = [ + "GREEN", + "BLUE", + "STONE", + "TEAL", + "VIOLET", + "TOXIC", + ] + return themes[Math.floor(Math.random() * themes.length)] + } + + findRandomMovementFlag() { + const flags = this.chunkNow?.objects.filter( + (f) => f instanceof Flag && f.type === "MOVEMENT", + ) + if (!flags) { + return undefined + } + + return flags.length > 0 + ? flags[Math.floor(Math.random() * flags.length)] + : undefined + } + + findRandomEmptyResourceFlag() { + const flags = this.objects.filter( + (f) => f instanceof Flag && f.type === "RESOURCE" && !f.target, + ) + return flags.length > 0 + ? flags[Math.floor(Math.random() * flags.length)] + : undefined + } +} diff --git a/src/lib/game/scenes/movingScene.ts b/src/lib/game/scenes/movingScene.ts new file mode 100644 index 00000000..cc630491 --- /dev/null +++ b/src/lib/game/scenes/movingScene.ts @@ -0,0 +1,62 @@ +import type { Game } from "../game" +import { GameScene } from "./gameScene" + +interface IMovingSceneOptions { + game: Game +} + +export class MovingScene extends GameScene { + constructor({ game }: IMovingSceneOptions) { + super({ + game, + }) + + void this.init() + } + + public async init() { + const village = this.initStartingVillage() + const wagonStartPoint = village.getWagonStopPoint() + + this.wagonService.initWagon(wagonStartPoint) + await this.initGroupPlayers() + + // void this.live() + } + + initStartingVillage() { + const initialOffsetX = 0 + const initialOffsetY = 2000 + const width = 5000 + const height = 1500 + const area = { + width, + height, + center: { + x: Math.round(width / 2 + initialOffsetX), + y: Math.round(height / 2 + initialOffsetY), + }, + } + const village = this.wagonService.routeService.generateRandomVillage({ + center: area.center, + width: area.width, + height: area.height, + theme: this.getRandomTheme(), + scene: this, + }) + this.chunks.push(village) + + return village + } + + async initGroupPlayers() { + if (!this.group) { + return + } + + for (const player of this.group.players) { + const instance = await this.initPlayer(player.id) + this.objects.push(instance) + } + } +} diff --git a/src/lib/game/scripts/buildScript.ts b/src/lib/game/scripts/buildScript.ts new file mode 100644 index 00000000..2116167e --- /dev/null +++ b/src/lib/game/scripts/buildScript.ts @@ -0,0 +1,34 @@ +import type { IGameObject, IGameTask, } from "$lib/game/types" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IBuildScriptOptions { + object: GameObject + target: IGameObject + buildFunc: () => boolean +} + +export class BuildScript extends Script { + constructor({ target, object, buildFunc }: IBuildScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.build(buildFunc), + ] + } + + build(func: () => boolean): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + const isFinished = func() + if (isFinished) { + this.markTaskAsDone() + } + }, + } + } +} diff --git a/src/lib/game/scripts/chopTreeScript.ts b/src/lib/game/scripts/chopTreeScript.ts new file mode 100644 index 00000000..117bc0af --- /dev/null +++ b/src/lib/game/scripts/chopTreeScript.ts @@ -0,0 +1,34 @@ +import type { IGameObject, IGameTask, } from "$lib/game/types" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IPlantNewTreeScriptOptions { + object: GameObject + target: IGameObject + chopTreeFunc: () => boolean +} + +export class ChopTreeScript extends Script { + constructor({ target, object, chopTreeFunc }: IPlantNewTreeScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.chopTree(chopTreeFunc), + ] + } + + chopTree(func: () => boolean): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + const isFinished = func() + if (isFinished) { + this.markTaskAsDone() + } + }, + } + } +} diff --git a/src/lib/game/scripts/mineStoneScript.ts b/src/lib/game/scripts/mineStoneScript.ts new file mode 100644 index 00000000..07c557ca --- /dev/null +++ b/src/lib/game/scripts/mineStoneScript.ts @@ -0,0 +1,34 @@ +import type { IGameObject, IGameTask, } from "$lib/game/types" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IMineStoneScriptOptions { + object: GameObject + target: IGameObject + mineStoneFunc: () => boolean +} + +export class MineStoneScript extends Script { + constructor({ target, object, mineStoneFunc }: IMineStoneScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.mineStone(mineStoneFunc), + ] + } + + mineStone(func: () => boolean): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + const isFinished = func() + if (isFinished) { + this.markTaskAsDone() + } + }, + } + } +} diff --git a/src/lib/game/scripts/moveOffScreenAndSelfDestroyScript.ts b/src/lib/game/scripts/moveOffScreenAndSelfDestroyScript.ts new file mode 100644 index 00000000..36325e77 --- /dev/null +++ b/src/lib/game/scripts/moveOffScreenAndSelfDestroyScript.ts @@ -0,0 +1,36 @@ +import type { IGameObject, IGameTask, } from "$lib/game/types" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IMoveOffScreenAndSelfDestroyScriptOptions { + object: GameObject + target: IGameObject + selfDestroyFunc: () => void +} + +export class MoveOffScreenAndSelfDestroyScript extends Script { + constructor({ + target, + object, + selfDestroyFunc, + }: IMoveOffScreenAndSelfDestroyScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.selfDestroy(selfDestroyFunc), + ] + } + + selfDestroy(func: () => void): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + func() + this.markTaskAsDone() + }, + } + } +} diff --git a/src/lib/game/scripts/moveToTargetScript.ts b/src/lib/game/scripts/moveToTargetScript.ts new file mode 100644 index 00000000..ea3132b8 --- /dev/null +++ b/src/lib/game/scripts/moveToTargetScript.ts @@ -0,0 +1,17 @@ +import type { IGameObject } from "$lib/game/types" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IMoveToRandomTargetScriptOptions { + object: GameObject + target: IGameObject +} + +export class MoveToTargetScript extends Script { + constructor({ target, object }: IMoveToRandomTargetScriptOptions) { + super({ object }) + + this.tasks = [this.setTarget(target), this.runToTarget()] + this.isInterruptible = true + } +} diff --git a/src/lib/game/scripts/moveToTradePostAndTradeScript.ts b/src/lib/game/scripts/moveToTradePostAndTradeScript.ts new file mode 100644 index 00000000..f5bcb7d1 --- /dev/null +++ b/src/lib/game/scripts/moveToTradePostAndTradeScript.ts @@ -0,0 +1,36 @@ +import type { IGameObject, IGameTask, } from "$lib/game/types" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IMoveToTradePostAndTradeScriptOptions { + object: GameObject + target: IGameObject + startTradeFunc: () => void +} + +export class MoveToTradePostAndTradeScript extends Script { + constructor({ + target, + object, + startTradeFunc, + }: IMoveToTradePostAndTradeScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.startTrade(startTradeFunc), + ] + } + + startTrade(func: () => void): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + func() + this.markTaskAsDone() + }, + } + } +} diff --git a/src/lib/game/scripts/placeItemInWarehouseScript.ts b/src/lib/game/scripts/placeItemInWarehouseScript.ts new file mode 100644 index 00000000..9f7dd760 --- /dev/null +++ b/src/lib/game/scripts/placeItemInWarehouseScript.ts @@ -0,0 +1,36 @@ +import type { IGameObject, IGameTask, } from "$lib/game/types" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IPlaceItemInWarehouseScriptOptions { + object: GameObject + target: IGameObject + placeItemFunc: () => void +} + +export class PlaceItemInWarehouseScript extends Script { + constructor({ + target, + object, + placeItemFunc, + }: IPlaceItemInWarehouseScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.placeItem(placeItemFunc), + ] + } + + placeItem(func: () => void): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + func() + this.markTaskAsDone() + }, + } + } +} diff --git a/src/lib/game/scripts/plantNewTreeScript.ts b/src/lib/game/scripts/plantNewTreeScript.ts new file mode 100644 index 00000000..254f0381 --- /dev/null +++ b/src/lib/game/scripts/plantNewTreeScript.ts @@ -0,0 +1,36 @@ +import type { IGameObject, IGameTask, } from "$lib/game/types" +import type { GameObject } from "../objects" +import { Script } from "./script" + +interface IPlantNewTreeScriptOptions { + object: GameObject + target: IGameObject + plantNewTreeFunc: () => void +} + +export class PlantNewTreeScript extends Script { + constructor({ + target, + object, + plantNewTreeFunc, + }: IPlantNewTreeScriptOptions) { + super({ object }) + + this.tasks = [ + this.setTarget(target), + this.runToTarget(), + this.plantNewTree(plantNewTreeFunc), + ] + } + + plantNewTree(func: () => void): IGameTask { + return { + id: "3", + status: "IDLE", + live: () => { + func() + this.markTaskAsDone() + }, + } + } +} diff --git a/src/lib/game/scripts/script.ts b/src/lib/game/scripts/script.ts new file mode 100644 index 00000000..f96a69fc --- /dev/null +++ b/src/lib/game/scripts/script.ts @@ -0,0 +1,84 @@ +import { createId } from "@paralleldrive/cuid2" +import type { IGameObject, IGameScript, IGameTask, } from "$lib/game/types" +import type { GameObject } from "../objects" + +interface IScriptOptions { + object: GameObject +} + +export class Script implements IGameScript { + public id: string + public tasks!: IGameScript["tasks"] + public isInterruptible = false + + public object!: GameObject + + constructor({ object }: IScriptOptions) { + this.id = createId() + this.object = object + } + + live() { + const activeTask = this.getActiveTask() + if (!activeTask) { + const nextTask = this.getNextIdleTask() + if (!nextTask) { + return this.markScriptAsFinished() + } + + return this.markTaskAsActive(nextTask) + } + + return activeTask.live() + } + + getActiveTask() { + return this.tasks.find((t) => t.status === "ACTIVE") + } + + getNextIdleTask() { + return this.tasks.find((t) => t.status === "IDLE") + } + + markTaskAsActive(task: IGameTask) { + task.status = "ACTIVE" + } + + markTaskAsDone() { + const activeTask = this.getActiveTask() + if (!activeTask) { + return + } + + activeTask.status = "DONE" + } + + markScriptAsFinished() { + this.object.script = undefined + } + + setTarget(target: IGameObject): IGameTask { + return { + id: createId(), + status: "IDLE", + live: () => { + this.object.target = target + this.object.state = "MOVING" + this.markTaskAsDone() + }, + } + } + + runToTarget(): IGameTask { + return { + id: createId(), + status: "IDLE", + live: () => { + const isMoving = this.object.move() + if (!isMoving) { + this.markTaskAsDone() + } + }, + } + } +} diff --git a/src/lib/game/services/actionService.ts b/src/lib/game/services/actionService.ts new file mode 100644 index 00000000..53662206 --- /dev/null +++ b/src/lib/game/services/actionService.ts @@ -0,0 +1,736 @@ +import type { + GameSceneType, + IGameActionResponse, + IGameSceneAction, + ItemType, +} from "$lib/game/types" +import type { Action } from "../actions/action" +import { Village } from "../chunks" +import { Group } from "../common" +import { Stone, Tree } from "../objects" +import type { Warehouse } from "../objects/buildings/warehouse" +import type { Player } from "../objects/units" +import type { GameScene } from "../scenes/gameScene" +import { ChopTreeScript } from "../scripts/chopTreeScript" +import { MineStoneScript } from "../scripts/mineStoneScript" +import { PlantNewTreeScript } from "../scripts/plantNewTreeScript" +import { + ADMIN_PLAYER_ID, + DISCORD_SERVER_INVITE_URL, + GITHUB_REPO_URL, + DONATE_URL +} from "$lib/config"; + +interface ICommandWithAction { + id: string + action: IGameSceneAction + command: string +} + +interface IActionServiceOptions { + scene: GameScene +} + +export const ANSWER = { + OK: { + ok: true, + message: null, + }, + DONATE_WOOD_OK: { + ok: true, + message: "You gave wood to the village! Your reputation has increased.", + }, + VOTED_OK: { + ok: true, + message: "You voted!", + }, + ERROR: { + ok: false, + message: null, + }, + BUSY_ERROR: { + ok: false, + message: "You're busy right now", + }, + CANT_DO_THIS_NOW_ERROR: { + ok: false, + message: "This cannot be done now.", + }, + NO_PLAYER_ERROR: { + ok: false, + message: "You are not in active game :(", + }, + NO_TARGET_ERROR: { + ok: false, + message: "No target specified.", + }, + NO_SPACE_AVAILABLE_ERROR: { + ok: false, + message: "No space available.", + }, + NO_AVAILABLE_TREE_ERROR: { + ok: false, + message: "No available tree", + }, + WRONG_AMOUNT_ERROR: { + ok: false, + message: "Incorrect quantity specified.", + }, + ALREADY_VOTED_ERROR: { + ok: false, + message: "You've already voted.", + }, + NOT_ENOUGH_PARAMS_ERROR: { + ok: false, + message: "Be more specific.", + }, + NOT_ENOUGH_WOOD_ERROR: { + ok: false, + message: "You don't have enough wood.", + }, +} + +export class ActionService { + public possibleCommands!: ICommandWithAction[] + public possibleActions!: IGameSceneAction[] + public activeActions!: IGameSceneAction[] + public scene: GameScene + + constructor({ scene }: IActionServiceOptions) { + this.scene = scene + + void this.initActions() + } + + async initActions() { + this.possibleActions = [ + "HELP", + "GIFT", + "TRADE", + "DONATE", + "REFUEL", + "STEAL_FUEL", + "CHOP", + "MINE", + "PLANT", + "START_GROUP_BUILD", + "DISBAND_GROUP", + "JOIN_GROUP", + "START_POLL", + "VOTE", + "START_CHANGING_SCENE", + "START_RAID", + "CREATE_NEW_PLAYER", + "START_CREATING_NEW_ADVENTURE", + "SHOW_MESSAGE", + "GITHUB", + ] + this.activeActions = this.possibleActions + } + + public findActionByCommand(command: string) { + return this.possibleCommands.find((a) => a.command === command) + } + + public findDynamicActionByCommand(command: string) { + const quest = this.scene.eventService.findActionByCommandInQuest(command) + if (quest) { + return quest + } + + const poll = this.scene.eventService.findActionByCommandInPoll(command) + if (poll) { + return poll + } + } + + public async handleAction( + action: IGameSceneAction, + playerId: string, + params?: string[], + ) { + const player = await this.scene.findOrCreatePlayer(playerId) + if (!player) { + return ANSWER.NO_PLAYER_ERROR + } + + this.scene.group.join(player) + player.updateLastActionAt() + + if (action === "SHOW_MESSAGE") { + return this.showMessageAction(player, params) + } + if (action === "REFUEL") { + return this.refuelAction(player, params) + } + if (action === "CHOP") { + return this.chopAction(player) + } + if (action === "MINE") { + return this.mineAction(player) + } + if (action === "PLANT") { + return this.plantAction(player) + } + if (action === "START_RAID") { + return this.startRaidAction(player, params) + } + if (action === "START_CHANGING_SCENE") { + // Admin only + if (player.id !== ADMIN_PLAYER_ID) { + return ANSWER.ERROR + } + return this.startChangingSceneAction(player, params) + } + if (action === "START_GROUP_BUILD") { + // Admin only + if (player.id !== ADMIN_PLAYER_ID) { + return ANSWER.ERROR + } + return this.startGroupBuildAction(player, params) + } + if (action === "DISBAND_GROUP") { + // Admin only + if (player.id !== ADMIN_PLAYER_ID) { + return ANSWER.ERROR + } + return this.disbandGroupAction() + } + if (action === "STEAL_FUEL") { + return this.stealFuelAction(player) + } + if (action === "HELP") { + return this.helpAction(player) + } + if (action === "GITHUB") { + return this.githubAction(player) + } + if (action === "DONATE") { + return this.donateAction(player) + } + if (action === "GIFT") { + return this.giftAction(player, params) + } + if (action === "TRADE") { + return this.tradeAction(player, params) + } + if (action === "CREATE_IDEA") { + return this.createIdeaAction(player, params) + } + + return ANSWER.ERROR + } + + public async handleDynamicAction( + action: Action, + playerId: string, + params: string[], + ): Promise { + const player = await this.scene.findOrCreatePlayer(playerId) + if (!player) { + return ANSWER.NO_PLAYER_ERROR + } + + this.scene.group.join(player) + player.updateLastActionAt() + + const answer = await action.live(player, params) + if (answer) { + return answer + } + + return ANSWER.ERROR + } + + public getAvailableCommands() { + const commands: string[] = [] + for (const action of this.activeActions) { + if (action === "HELP") { + commands.push("!помощь") + } + if (action === "REFUEL") { + commands.push("!заправить [кол-во]") + } + if (action === "CHOP") { + commands.push("!рубить") + } + if (action === "MINE") { + commands.push("!добыть") + } + if (action === "GIFT") { + commands.push("!подарить [название] [кол-во]") + } + if (action === "DONATE") { + commands.push("!донат") + } + } + + return commands + } + + public isActionPossible(action: IGameSceneAction): boolean { + return !!this.activeActions.find((a) => a === action) + } + + private startRaidAction(player: Player, params?: string[]) { + // First param is raidersCount + const raidersCount = params ? Number(params[0]) : 0 + + this.scene.eventService.init({ + title: "The raid has started!", + description: "", + type: "RAID_STARTED", + secondsToEnd: 60 * 5, + }) + this.scene.initRaiders(raidersCount) + + // Raider points + void player.addRaiderPoints(raidersCount) + + return ANSWER.OK + } + + private async showMessageAction(player: Player, params?: string[]) { + if (!this.isActionPossible("SHOW_MESSAGE")) { + return ANSWER.ERROR + } + + if (!params || !params[0]) { + return ANSWER.ERROR + } + + const message = params[0] + player.addMessage(message) + + return ANSWER.OK + } + + private async stealFuelAction(player: Player) { + if (!this.isActionPossible("STEAL_FUEL")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + this.scene.wagonService.wagon.emptyFuel() + + await player.addVillainPoints(1) + + return { + ok: true, + message: `${player.userName}, and you're a villain!`, + } + } + + private async refuelAction(player: Player, params?: string[]) { + if (!this.isActionPossible("REFUEL")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + if (!params) { + return ANSWER.NO_TARGET_ERROR + } + + const count = this.getAmountFromChatCommand(params[0]) + if (!count) { + return ANSWER.WRONG_AMOUNT_ERROR + } + + const items = player.inventory?.items ?? [] + const itemExist = items.find((item) => item.type === "WOOD") + if (!itemExist) { + return { + ok: false, + message: `${player.userName}, you don't have wood.`, + } + } + + const isSuccess = await player.inventory?.reduceOrDestroyItem( + itemExist.type, + count, + ) + if (!isSuccess) { + return { + ok: false, + message: `${player.userName}, not enough wood.`, + } + } + + await player.addRefuellerPoints(count) + + this.scene.wagonService.wagon.refuel(count) + + return { + ok: true, + message: `${player.userName}, you helped refuel the Wagon.`, + } + } + + getAmountFromChatCommand(text: string): number | null { + if (typeof Number(text) === "number" && Number(text) > 0) { + return Math.round(Number(text)) + } + + return null + } + + private async chopAction(player: Player) { + if (!this.isActionPossible("CHOP")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + if (player.script && !player.script.isInterruptible) { + return ANSWER.BUSY_ERROR + } + + const target = this.scene.getTreeToChop() + if (!target) { + return ANSWER.NO_AVAILABLE_TREE_ERROR + } + + const chopTreeFunc = (): boolean => { + void player.chopTree() + if (!player.target || player.target.state === "DESTROYED") { + player.state = "IDLE" + if (player.target instanceof Tree) { + void player.inventory.addOrCreateItem("WOOD", player.target?.resource) + } + return true + } + return false + } + + player.script = new ChopTreeScript({ + object: player, + target, + chopTreeFunc, + }) + + return ANSWER.OK + } + + private async mineAction(player: Player) { + if (!this.isActionPossible("MINE")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + if (player.script && !player.script.isInterruptible) { + return { + ok: false, + message: `${player.userName}, you're busy right now.`, + } + } + + const target = this.scene.getStoneToMine() + if (!target) { + return { + ok: false, + message: `${player.userName}, there is no available stone.`, + } + } + + const mineStoneFunc = (): boolean => { + void player.mineStone() + if (!player.target || player.target.state === "DESTROYED") { + player.state = "IDLE" + if (player.target instanceof Stone) { + void player.inventory.addOrCreateItem( + "STONE", + player.target?.resource, + ) + } + return true + } + return false + } + + player.script = new MineStoneScript({ + object: player, + target, + mineStoneFunc, + }) + + return ANSWER.OK + } + + private plantAction(player: Player) { + if (!this.isActionPossible("PLANT")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + if (player.script && !player.script.isInterruptible) { + return { + ok: false, + message: `${player.userName}, you're busy right now.`, + } + } + + if (this.scene.chunkNow instanceof Village) { + const target = this.scene.chunkNow.checkIfNeedToPlantTree() + if (!target) { + return { + ok: false, + message: `${player.userName}, no space available.`, + } + } + + const plantNewTreeFunc = () => { + if (this.scene.chunkNow instanceof Village) { + this.scene.chunkNow.plantNewTree(target) + } + } + + player.script = new PlantNewTreeScript({ + object: player, + target, + plantNewTreeFunc, + }) + + return ANSWER.OK + } + + return ANSWER.ERROR + } + + private startChangingSceneAction(_: Player, params?: string[]) { + if (!this.isActionPossible("START_CHANGING_SCENE")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + if (!params) { + return ANSWER.NO_TARGET_ERROR + } + + const scene = this.getSceneTypeFromChatCommand(params[1]) + if (!scene) { + return ANSWER.NO_TARGET_ERROR + } + + this.scene.eventService.init({ + type: "SCENE_CHANGING_STARTED", + title: "Changing location", + description: "", + scene, + secondsToEnd: 10, + }) + + return ANSWER.OK + } + + getSceneTypeFromChatCommand(text: string): GameSceneType | null { + if (text === "деревня" || text === "деревню") { + return "VILLAGE" + } + if (text === "защиту" || text === "защита") { + return "DEFENCE" + } + + return null + } + + private startGroupBuildAction(_: Player, params?: string[]) { + if (!this.isActionPossible("START_GROUP_BUILD")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + if (!params) { + return ANSWER.NO_TARGET_ERROR + } + + const scene = this.getSceneTypeFromChatCommand(params[1]) + if (!scene) { + return ANSWER.NO_TARGET_ERROR + } + + this.scene.group = new Group() + + this.scene.eventService.init({ + type: "GROUP_FORM_STARTED", + title: "The group is recruiting!", + description: "", + scene, + secondsToEnd: 120, + }) + + return ANSWER.OK + } + + private disbandGroupAction() { + if (!this.isActionPossible("DISBAND_GROUP")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + this.scene.group?.disband() + + return { + ok: true, + message: "The group has been disbanded!", + } + } + + private helpAction(player: Player) { + if (!this.isActionPossible("HELP")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + return { + ok: true, + message: `${player.userName}, this is an interactive chat game that any viewer can participate in! Basic commands: !chop, !mine. The remaining commands appear in events (on the right of the screen). Join our community: ${DISCORD_SERVER_INVITE_URL}`, + } + } + + private githubAction(player: Player) { + if (!this.isActionPossible("GITHUB")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + return { + ok: true, + message: `${player.userName}, the game code is in the repository: ${GITHUB_REPO_URL}`, + } + } + + private donateAction(player: Player) { + if (!this.isActionPossible("DONATE")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + return { + ok: true, + message: `${player.userName}, support the game: ${DONATE_URL}`, + } + } + + private async giftAction(player: Player, params: string[] | undefined) { + if (!this.isActionPossible("GIFT")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + if (!params) { + return { + ok: false, + message: `${player.userName}, be more specific.`, + } + } + + const item = this.getItemTypeFromChatCommand(params[0]) + if (!item) { + return { + ok: false, + message: `${player.userName}, be more specific.`, + } + } + + const amount = this.getAmountFromChatCommand(params[1]) + if (!amount) { + return { + ok: false, + message: `${player.userName}, be more specific.`, + } + } + + let warehouse: Warehouse | undefined + if (this.scene.chunkNow instanceof Village) { + warehouse = this.scene.chunkNow.getWarehouse() + } + + if (item === "WOOD") { + const isSuccess = await player.inventory.reduceOrDestroyItem(item, amount) + if (!isSuccess) { + return { + ok: false, + message: `${player.userName}, you don't have enough wood.`, + } + } + + await warehouse?.inventory.addOrCreateItem(item, amount) + await player.addReputation(amount) + + return { + ok: true, + message: `${player.userName}, you gave wood to the village! Your reputation has increased.`, + } + } + if (item === "STONE") { + const isSuccess = await player.inventory.reduceOrDestroyItem(item, amount) + if (!isSuccess) { + return { + ok: false, + message: `${player.userName}, you don't have enough stone.`, + } + } + + await warehouse?.inventory.addOrCreateItem(item, amount) + await player.addReputation(amount) + + return { + ok: true, + message: `${player.userName}, you gave stones to the village! Your reputation has increased.`, + } + } + + return { + ok: false, + message: `${player.userName}, be more specific.`, + } + } + + getItemTypeFromChatCommand(text: string): ItemType | null { + if (text === "wood") { + return "WOOD" + } + if (text === "stone") { + return "STONE" + } + if (text === "axe") { + return "AXE" + } + if (text === "pickaxe") { + return "PICKAXE" + } + + return null + } + + private async tradeAction(player: Player, params: string[] | undefined) { + if (!this.isActionPossible("TRADE")) { + return ANSWER.CANT_DO_THIS_NOW_ERROR + } + + if (!params) { + return ANSWER.NO_TARGET_ERROR + } + + const amount = this.getAmountFromChatCommand(params[1]) + if (!amount) { + return ANSWER.WRONG_AMOUNT_ERROR + } + + const status = await this.scene.tradeService.findActiveOfferAndTrade( + params[0], + amount, + player, + ) + if (status === "OFFER_ERROR") { + return { + ok: false, + message: "Something is wrong. The deal fell through.", + } + } + if (status === "OFFER_NOT_FOUND") { + return ANSWER.NO_TARGET_ERROR + } + + return { + ok: true, + message: `${player.userName}, successful trade deal!`, + } + } + + private createIdeaAction(player: Player, params: string[] | undefined) { + const text = params ? params[0] : "" + + this.scene.eventService.init({ + title: "New idea from Twitch Viewer!", + description: `${player.userName}: ${text}`, + type: "IDEA_CREATED", + secondsToEnd: 60 * 3, + }) + + return ANSWER.OK + } +} diff --git a/src/lib/game/services/eventService.ts b/src/lib/game/services/eventService.ts new file mode 100644 index 00000000..42eaaf76 --- /dev/null +++ b/src/lib/game/services/eventService.ts @@ -0,0 +1,260 @@ +import type { + GameSceneType, + IGameEvent, + IGamePoll, + IGameQuest, + IGameQuestTask, +} from "$lib/game/types" +import type { Action } from "../actions/action" +import { Village } from "../chunks" +import { Event } from "../common" +import type { GameScene } from "../scenes/gameScene" +import { PollService } from "./pollService" +import { QuestService } from "./questService" + +interface IEventServiceOptions { + scene: GameScene +} + +export class EventService { + public events: Event[] = [] + public questService: QuestService + public pollService: PollService + public scene: GameScene + + constructor({ scene }: IEventServiceOptions) { + this.scene = scene + this.questService = new QuestService({ scene }) + this.pollService = new PollService({ scene }) + } + + public update() { + for (const event of this.events) { + const status = event.checkStatus() + + if (status === "STOPPED") { + this.handleEnding(event) + this.destroy(event) + } + + this.updateSuccessPollsWithQuest(event) + this.updateClosedQuests(event) + } + + this.pollService.update() + this.questService.update() + } + + public init({ + title, + description, + type, + secondsToEnd, + scene, + poll, + quest, + offers, + }: { + title: IGameEvent["title"] + description: IGameEvent["description"] + type: IGameEvent["type"] + secondsToEnd: number + scene?: GameSceneType + poll?: IGameEvent["poll"] + quest?: IGameEvent["quest"] + offers?: IGameEvent["offers"] + }) { + const event = new Event({ + title, + description, + type, + secondsToEnd, + scene, + poll, + quest, + offers, + }) + + this.events.push(event) + } + + public getEvents(): IGameEvent[] { + return this.events.map((event) => ({ + id: event.id, + title: event.title, + description: event.description, + type: event.type, + status: event.status, + endsAt: event.endsAt, + poll: this.preparePollData(event.poll), + quest: this.prepareQuestData(event.quest), + offers: event.offers, + })) + } + + prepareQuestData(quest: IGameQuest | undefined) { + if (!quest) { + return + } + + const tasks = quest?.tasks.map((t) => { + const action = t.action + ? { + ...t.action, + live: undefined, + scene: undefined, + } + : undefined + return { ...t, action } + }) + + return { + ...quest, + tasks, + } + } + + preparePollData(poll: IGamePoll | undefined) { + if (!poll) { + return + } + + return { + ...poll, + action: { + ...poll.action, + poll: undefined, + live: undefined, + scene: undefined, + }, + scene: undefined, + } + } + + public destroy(event: Event) { + const index = this.events.indexOf(event) + this.events.splice(index, 1) + } + + public findActionByCommandInQuest(command: string) { + for (const event of this.events) { + if (event.quest?.tasks) { + const task = event.quest.tasks.find( + (q) => q.action?.command === command, + ) + if (task?.action) { + return task.action as Action + } + } + } + } + + public findActionByCommandInPoll(command: string) { + for (const event of this.events) { + if (event.poll?.action && event.poll.action.command === command) { + return event.poll?.action as Action + } + } + } + + private handleEnding(event: Event) { + if (event.type === "SCENE_CHANGING_STARTED" && event.scene) { + this.scene.game.initScene(event.scene) + } + if (event.type === "GROUP_FORM_STARTED" && event.scene) { + this.scene.game.initScene(event.scene) + } + if (event.type === "RAID_STARTED") { + this.scene.stopRaid() + } + if (event.type === "TRADE_STARTED") { + this.scene.tradeService.handleTradeIsOver() + } + if (event.type === "VOTING_FOR_NEW_MAIN_QUEST_STARTED") { + this.scene.tradeService.handleTradeIsOver() + } + } + + private destroyAllEventsWithPoll() { + for (const event of this.events) { + if (event.poll) { + this.destroy(event) + } + } + } + + private updateSuccessPollsWithQuest(event: Event) { + if (event.poll?.status !== "SUCCESS" || !event.quest) { + return + } + + const updateProgress1: IGameQuestTask["updateProgress"] = () => { + if ( + !this.scene.wagonService.routeService.route?.flags && + this.events.find((e) => e.type === "MAIN_QUEST_STARTED") + ) { + return { + status: "SUCCESS", + } + } + + const items = + this.scene.wagonService.wagon.cargo?.checkIfAlreadyHaveItem("WOOD") + if (!items) { + return { + status: "FAILED", + progressNow: 0, + } + } + + return { + status: "ACTIVE", + progressNow: items.amount, + } + } + + const tasks = [ + this.questService.createTask({ + updateProgress: updateProgress1, + description: "Transport cargo safely", + progressNow: 100, + progressToSuccess: 60, + }), + ] + + this.init({ + title: "Journey", + description: "", + type: "MAIN_QUEST_STARTED", + secondsToEnd: event.quest.conditions.limitSeconds ?? 9999999, + quest: { + ...event.quest, + status: "ACTIVE", + tasks, + }, + }) + + // Cargo + this.scene.wagonService.wagon.setCargo() + + if (this.scene.chunkNow instanceof Village) { + this.scene.wagonService.routeService.generateAdventure( + this.scene.chunkNow, + event.quest.conditions.chunks ?? 3, + ) + } + + this.scene.tradeService.traderIsMovingWithWagon = true + + this.destroyAllEventsWithPoll() + } + + private updateClosedQuests(event: Event) { + if (event.status === "STARTED" && event.quest) { + if (event.quest.status === "FAILED" || event.quest.status === "SUCCESS") { + // + event.status = "STOPPED" + } + } + } +} diff --git a/src/lib/game/services/pollService.ts b/src/lib/game/services/pollService.ts new file mode 100644 index 00000000..cbd1deb9 --- /dev/null +++ b/src/lib/game/services/pollService.ts @@ -0,0 +1,50 @@ +import type { IGameObjectPlayer, IGamePoll, } from "$lib/game/types" +import type { Player } from "../objects/units" +import type { GameScene } from "../scenes/gameScene" + +interface IPollServiceOptions { + scene: GameScene +} + +export class PollService { + public scene: GameScene + + constructor({ scene }: IPollServiceOptions) { + this.scene = scene + } + + public update() { + for (const event of this.scene.eventService.events) { + if (!event.poll || event.poll.status !== "ACTIVE") { + continue + } + + if (event.poll.votes.length >= event.poll.votesToSuccess) { + event.poll.status = "SUCCESS" + } + } + } + + public findActivePollAndVote(pollId: string, player: Player) { + for (const event of this.scene.eventService.events) { + if (event.poll && event.poll?.id === pollId) { + const voted = this.vote(event.poll, player) + if (!voted) { + return "VOTED_ALREADY" + } + return "VOTE_SUCCESS" + } + } + + return "POLL_NOT_FOUND" + } + + private vote(poll: IGamePoll, player: IGameObjectPlayer): boolean { + if (poll.votes.find((v) => v.id === player.id)) { + return false + } + + poll.votes.push({ id: player.id, userName: player.userName }) + return true + } +} diff --git a/src/lib/game/services/questService.ts b/src/lib/game/services/questService.ts new file mode 100644 index 00000000..1a14e8bb --- /dev/null +++ b/src/lib/game/services/questService.ts @@ -0,0 +1,252 @@ +import { createId } from "@paralleldrive/cuid2" +import type { + IGameQuest, + IGameQuestTask, + IGameQuestTaskFunc, +} from "$lib/game/types" +import { DonateWoodToVillageAction } from "../actions/donateWoodToVillageAction" +import { PlantTreeAction } from "../actions/plantTreeAction" +import { Village } from "../chunks" +import { NoTradingPostQuest } from "../quests/noTradingPostQuest" +import { TreesAreRunningOutQuest } from "../quests/treesAreRunningOutQuest" +import type { GameScene } from "../scenes/gameScene" + +interface IQuestServiceOptions { + scene: GameScene +} + +export class QuestService { + public scene: GameScene + + constructor({ scene }: IQuestServiceOptions) { + this.scene = scene + } + + public update() { + this.updateAndFinishActiveQuests() + + if (this.scene.chunkNow instanceof Village) { + this.generateNewSideQuest() + } + + for (const event of this.scene.eventService.events) { + if (!event.quest) { + continue + } + + for (const task of event.quest.tasks) { + if (task.status === "ACTIVE") { + this.updateQuestActiveTask(task) + } + } + } + } + + public create({ + status, + type, + tasks, + conditions, + creatorId, + title, + description, + }: Omit): IGameQuest { + return { + id: createId(), + status, + title, + description, + type, + tasks, + conditions, + creatorId, + } + } + + public createTask({ + updateProgress, + progressToSuccess, + progressNow, + description, + }: Pick< + IGameQuestTask, + "description" | "progressToSuccess" | "progressNow" | "updateProgress" + >): IGameQuestTask { + return { + id: createId(), + status: "ACTIVE", + description, + progressNow, + progressToSuccess, + updateProgress, + } + } + + private updateQuestActiveTask(task: IGameQuestTask) { + const progress = task.updateProgress(task.progressToSuccess) + + if (typeof progress.status !== "undefined") { + task.status = progress.status + } + if (typeof progress.progressNow !== "undefined") { + task.progressNow = progress.progressNow + } + if (typeof progress.progressToSuccess !== "undefined") { + task.progressToSuccess = progress.progressToSuccess + } + } + + private updateAndFinishActiveQuests() { + for (const event of this.scene.eventService.events) { + if (!event.quest || event.quest.status !== "ACTIVE") { + continue + } + + // Tasks done? + if (!event.quest.tasks.find((t) => t.status === "ACTIVE")) { + // + this.scene.wagonService.wagon.emptyCargo() + this.scene.tradeService.traderIsMovingWithWagon = false + + if (!event.quest.tasks.find((t) => t.status === "FAILED")) { + // Reward + event.quest.status = "SUCCESS" + continue + } + + event.quest.status = "FAILED" + } + } + } + + private generateSecondSideQuest() { + const sideQuests = this.scene.eventService.events.filter( + (e) => e.type === "SIDE_QUEST_STARTED", + ) + if (sideQuests.length >= 1) { + return + } + + const taskUpdateFunc1: IGameQuestTask["updateProgress"] = () => { + if (this.scene.chunkNow instanceof Village) { + const treesAmount = this.scene.chunkNow.getTreesAmount() + if (treesAmount >= 30) { + return { + status: "SUCCESS", + progressNow: treesAmount, + } + } + + return { + status: "ACTIVE", + progressNow: treesAmount, + } + } + + return { + status: "ACTIVE", + } + } + + const taskAction1 = new PlantTreeAction({ scene: this.scene }) + + const quest = new TreesAreRunningOutQuest({ + creatorId: "1", + taskUpdateFunc1, + taskAction1, + }) + + this.scene.eventService.init({ + type: "SIDE_QUEST_STARTED", + title: quest.title, + description: quest.description, + secondsToEnd: 9999999, + quest, + }) + } + + private generateNewSideQuest() { + if (!this.scene.chunkNow) { + return + } + + if (this.scene.chunkNow instanceof Village) { + const store = this.scene.chunkNow.getStore() + if (store) { + const notEnough = this.scene.chunkNow.checkIfThereAreNotEnoughTrees() + if (notEnough) { + return this.generateSecondSideQuest() + } + + return + } + } + + const sideQuests = this.scene.eventService.events.filter( + (e) => e.type === "SIDE_QUEST_STARTED", + ) + if (sideQuests.length >= 1) { + return + } + + const taskUpdateFunc1: IGameQuestTaskFunc = () => { + if (this.scene.chunkNow instanceof Village) { + const warehouse = this.scene.chunkNow.getWarehouse() + if (warehouse) { + const wood = warehouse.getItemByType("WOOD") + if (wood?.amount) { + if (wood.amount >= 25) { + return { + status: "SUCCESS", + progressNow: wood.amount, + } + } + + return { + status: "ACTIVE", + progressNow: wood.amount, + } + } + } + } + + return { + status: "ACTIVE", + } + } + + const taskAction1 = new DonateWoodToVillageAction({ scene: this.scene }) + + const taskUpdateFunc2: IGameQuestTaskFunc = () => { + if (this.scene.chunkNow instanceof Village) { + const store = this.scene.chunkNow.getStore() + if (store) { + return { + status: "SUCCESS", + progressNow: true, + } + } + } + + return { + status: "ACTIVE", + progressNow: false, + } + } + + const quest = new NoTradingPostQuest({ + creatorId: "1", + taskUpdateFunc1, + taskUpdateFunc2, + taskAction1, + }) + + this.scene.eventService.init({ + type: "SIDE_QUEST_STARTED", + title: quest.title, + description: quest.description, + secondsToEnd: 9999999, + quest, + }) + } +} diff --git a/src/lib/game/services/routeService.ts b/src/lib/game/services/routeService.ts new file mode 100644 index 00000000..07d169f1 --- /dev/null +++ b/src/lib/game/services/routeService.ts @@ -0,0 +1,218 @@ +import { type IGameChunkTheme, type IGameRoute, } from "$lib/game/types" +import { Forest, LakeChunk, Village } from "../chunks" +import { Route } from "../common" +import { Stone, Tree } from "../objects" +import type { GameScene } from "../scenes/gameScene" +import { getRandomInRange } from "$lib/random"; + +interface IRouteServiceOptions { + scene: GameScene +} + +export class RouteService { + public route: Route | undefined + public scene: GameScene + + constructor({ scene }: IRouteServiceOptions) { + this.scene = scene + } + + public update() { + if (!this.route?.flags || this.route.flags.length <= 0) { + if ( + this.scene.eventService.events.find( + (e) => e.type === "MAIN_QUEST_STARTED", + ) + ) { + return this.finishAdventure() + } + } + + if (this.route) { + for (const flag of this.route.flags) { + void flag.live() + } + } + } + + public getRoute(): IGameRoute | null { + if (!this.route) { + return null + } + + return { + startPoint: this.route.startPoint, + endPoint: this.route.endPoint, + chunks: this.route.chunks, + } + } + + generateAdventure(village: Village, chunks: number) { + const wagonStartPoint = village.getWagonStopPoint() + const villageOutPoint = village.getRandomOutPoint() + + this.route = new Route({ scene: this.scene }) + this.route.addGlobalFlag(wagonStartPoint) + this.route.startPoint = wagonStartPoint + this.route.addChunk(village) + + this.generateChunks({ x: villageOutPoint.x, y: villageOutPoint.y }, chunks) + this.markObjectsAsOnWagonPath(this.route) + } + + generateChunks(startPoint: { x: number; y: number }, amount: number) { + let outPoint = startPoint + + for (let i = 1; i <= amount; i++) { + const chunk = this.generateRandomChunk(outPoint) + outPoint = chunk.getRandomOutPoint() + this.route?.addGlobalFlag(outPoint) + this.route?.addChunk(chunk) + } + + // Generate last chunk + const finalVillageWidth = 3600 + const finalVillage = this.generateRandomVillage({ + center: { x: outPoint.x + finalVillageWidth / 2, y: outPoint.y }, + width: finalVillageWidth, + height: 2000, + theme: this.getRandomTheme(), + scene: this.scene, + }) + this.scene.chunks.push(finalVillage) + const stopPoint = finalVillage.getWagonStopPoint() + this.route?.addGlobalFlag(stopPoint) + this.route?.addChunk(finalVillage) + this.route?.setEndPoint(stopPoint) + } + + generateRandomChunk(startPoint: { x: number; y: number }) { + const random = getRandomInRange(1, 2) + + const width = getRandomInRange(2000, 3000) + const height = getRandomInRange(2500, 3500) + const center = { + x: startPoint.x + width / 2, + y: startPoint.y, + } + + switch (random) { + case 1: + return this.generateRandomForest({ + center, + width, + height, + theme: this.getRandomTheme(), + }) + case 2: + return this.generateRandomLake({ + center, + width, + height, + theme: this.getRandomTheme(), + }) + } + + return this.generateRandomForest({ + center, + width, + height, + theme: this.getRandomTheme(), + }) + } + + markObjectsAsOnWagonPath(route: Route) { + for (const chunk of this.scene.chunks) { + for (const object of chunk.objects) { + if (object instanceof Tree || object instanceof Stone) { + const isOnPath = route.checkIfPointIsOnWagonPath({ + x: object.x, + y: object.y, + }) + if (isOnPath) { + object.isOnWagonPath = true + } + } + } + } + } + + generateRandomVillage({ + center, + width, + height, + theme, + scene, + }: { + center: { x: number; y: number } + width: number + height: number + theme: IGameChunkTheme + scene: GameScene + }) { + return new Village({ width, height, center, theme, scene }) + } + + generateRandomForest({ + center, + width, + height, + theme, + }: { + center: { x: number; y: number } + width: number + height: number + theme: IGameChunkTheme + }) { + const forest = new Forest({ + scene: this.scene, + width, + height, + center, + theme + }) + this.scene.chunks.push(forest) + return forest + } + + generateRandomLake({ + center, + width, + height, + theme, + }: { + center: { x: number; y: number } + width: number + height: number + theme: IGameChunkTheme + }) { + const lake = new LakeChunk({ width, height, center, theme }) + this.scene.chunks.push(lake) + return lake + } + + getRandomTheme(): IGameChunkTheme { + const themes: IGameChunkTheme[] = [ + "GREEN", + "BLUE", + "STONE", + "TEAL", + "VIOLET", + "TOXIC", + ] + return themes[Math.floor(Math.random() * themes.length)] + } + + finishAdventure() { + console.log("Adventure finished!", new Date()) + this.route = undefined + this.scene.wagonService.wagon.emptyCargo() + this.scene.tradeService.traderIsMovingWithWagon = false + this.scene.tradeService.handleTradeIsOver() + + if (this.scene.chunkNow?.id) { + const id = this.scene.chunkNow.id + this.scene.chunks = this.scene.chunks.filter((chunk) => chunk.id === id) + } + } +} diff --git a/src/lib/game/services/tradeService.ts b/src/lib/game/services/tradeService.ts new file mode 100644 index 00000000..07654569 --- /dev/null +++ b/src/lib/game/services/tradeService.ts @@ -0,0 +1,319 @@ +import { type ITradeOffer } from "$lib/game/types" +import { Village } from "../chunks" +import { Poll } from "../common" +import { Flag } from "../objects" +import type { Player } from "../objects/units" +import { Trader } from "../objects/units" +import type { GameScene } from "../scenes/gameScene" +import { + MoveOffScreenAndSelfDestroyScript +} from "../scripts/moveOffScreenAndSelfDestroyScript" +import { MoveToTargetScript } from "../scripts/moveToTargetScript" +import { + MoveToTradePostAndTradeScript +} from "../scripts/moveToTradePostAndTradeScript" +import { getRandomInRange } from "$lib/random"; + +interface ITradeServiceOptions { + scene: GameScene +} + +export class TradeService { + public offers: ITradeOffer[] = [] + public tradeWasSuccessful: boolean + public traderIsMovingWithWagon: boolean + public scene: GameScene + + constructor({ scene }: ITradeServiceOptions) { + this.scene = scene + this.traderIsMovingWithWagon = false + this.tradeWasSuccessful = false + } + + public update() { + this.checkAndGenerateTrader() + this.checkClosedOffers() + } + + public getTrader() { + return this.scene.objects.find((obj) => obj instanceof Trader) as + | Trader + | undefined + } + + public getStore() { + if (this.scene.chunkNow instanceof Village) { + return this.scene.chunkNow.getStore() + } + } + + public async findActiveOfferAndTrade( + offerId: string, + amount: number, + player: Player, + ) { + for (const offer of this.offers) { + if (offer.id === offerId) { + const status = await this.trade(offer, amount, player) + if (!status) { + return "OFFER_ERROR" + } + return "OFFER_SUCCESS" + } + } + + return "OFFER_NOT_FOUND" + } + + public async trade( + offer: ITradeOffer, + amount: number, + player: Player, + ): Promise { + if (offer.amount < amount) { + return false + } + + if (offer.type === "BUY") { + const item = player.inventory.checkIfAlreadyHaveItem(offer.item) + if (!item || item.amount < amount) { + return false + } + + await player.updateCoins(offer.unitPrice * amount) + await player.inventory.reduceOrDestroyItem(item.type, amount) + + offer.amount -= amount + return true + } + + return false + } + + public updateTrader(object: Trader) { + object.live() + + if (!object.script) { + if (this.traderIsMovingWithWagon) { + // Moving near Wagon + const random = getRandomInRange(1, 150) + if (random <= 1) { + const target = this.scene.wagonService.findRandomNearFlag() + object.script = new MoveToTargetScript({ + object, + target, + }) + return + } + } + + // Moving to Trade + if (this.checkIfNeedToStartTrade()) { + if (this.scene.chunkNow instanceof Village) { + const target = this.getTradePointFlag() + if (target) { + const startTradeFunc = () => { + this.scene.eventService.init({ + title: "Trade in progress", + description: "", + type: "TRADE_STARTED", + secondsToEnd: 60 * 6, + offers: this.offers, + }) + } + + object.script = new MoveToTradePostAndTradeScript({ + object, + target, + startTradeFunc, + }) + return + } + } + } + } + } + + public handleTradeIsOver() { + const trader = this.getTrader() + if (!trader) { + return + } + + const target = this.scene.wagonService.findRandomOutFlag() + const selfDestroyFunc = () => { + this.scene.removeObject(trader) + } + + trader.script = new MoveOffScreenAndSelfDestroyScript({ + object: trader, + target, + selfDestroyFunc, + }) + + this.removeTrade() + } + + public removeTrade() { + this.offers = [] + for (const event of this.scene.eventService.events) { + if (event.type === "TRADE_STARTED") { + this.scene.eventService.destroy(event) + } + } + } + + private checkIfNeedToStartTrade(): boolean { + if (this.tradeWasSuccessful) { + return false + } + + const activeTrade = this.scene.eventService.events.find( + (e) => e.type === "TRADE_STARTED", + ) + return !activeTrade + } + + private getTradePointFlag() { + return this.scene.chunkNow?.objects.find( + (obj) => obj instanceof Flag && obj.type === "TRADE_POINT", + ) as Flag | undefined + } + + private createTradeTargetFlagNearStore() { + if (this.getTradePointFlag()) { + return + } + + const store = this.getStore() + if (!store) { + return + } + + const flag = new Flag({ + scene: this.scene, + x: store.x + 105, + y: store.y + 10, + type: "TRADE_POINT", + }) + this.scene.chunkNow?.objects.push(flag) + } + + private checkAndGenerateTrader() { + if (this.scene.chunkNow instanceof Village) { + const store = this.scene.chunkNow.getStore() + const trader = this.getTrader() + if (store?.id && !trader?.id) { + this.createTradeTargetFlagNearStore() + this.generateNewTrader() + this.generateTradeOffers() + } + } + } + + private checkClosedOffers() { + for (const offer of this.offers) { + if (offer.amount === 0) { + this.tradeWasSuccessful = true + this.removeTrade() + this.generateNewMainQuest() + } + } + } + + private generateNewTrader() { + const { x, y } = this.scene.wagonService.findRandomOutFlag() + const trader = new Trader({ scene: this.scene, x, y }) + + this.scene.objects.push(trader) + } + + private generateTradeOffers() { + const offersAmount = 1 + const offers = [] + + for (let i = 1; i <= offersAmount; i++) { + const id = this.generateId() + const commandToTrade = `!trade ${id}` + + const offer: ITradeOffer = { + id, + type: "BUY", + amount: 25, + unitPrice: 1, + item: "WOOD", + commandToTrade, + } + + offers.push(offer) + } + + this.offers.push(...offers) + this.tradeWasSuccessful = false + } + + private generateId(startId = 1): string { + const id = startId + for (const offer of this.offers) { + if (offer.id === id.toString()) { + const nextTry = id + 1 + return this.generateId(nextTry) + } + } + return id.toString() + } + + generateNewMainQuest() { + const trader = this.getTrader() + if (!trader) { + return + } + + const store = this.getStore() + if (!store) { + return + } + + const votingEvents = this.scene.eventService.events.filter( + (e) => e.type === "VOTING_FOR_NEW_MAIN_QUEST_STARTED", + ) + if (votingEvents.length >= 1) { + return + } + + const adventureEvents = this.scene.eventService.events.filter( + (e) => e.type === "MAIN_QUEST_STARTED", + ) + if (adventureEvents.length >= 1) { + return + } + + const votesToSuccess = + this.scene.findActivePlayers().length >= 2 + ? this.scene.findActivePlayers().length + : 1 + + const poll = new Poll({ votesToSuccess, scene: this.scene }) + + this.scene.eventService.init({ + type: "VOTING_FOR_NEW_MAIN_QUEST_STARTED", + title: "The merchant offers a quest", + description: "Let's make the quest active? Vote in chat.", + secondsToEnd: 180, + quest: this.scene.eventService.questService.create({ + status: "INACTIVE", + type: "MAIN", + title: "Transport cargo to a neighboring village", + description: + "The merchant is worried about the safety of the items in the chest.", + creatorId: trader.id, + tasks: [], + conditions: { + chunks: getRandomInRange(3, 5), + limitSeconds: 3000, + }, + }), + poll, + }) + } +} diff --git a/src/lib/game/services/wagonService.ts b/src/lib/game/services/wagonService.ts new file mode 100644 index 00000000..cf6addc0 --- /dev/null +++ b/src/lib/game/services/wagonService.ts @@ -0,0 +1,157 @@ +import { Flag, Wagon } from "../objects" +import type { GameScene } from "../scenes/gameScene" +import { RouteService } from "./routeService" +import { getRandomInRange, getMinusOrPlus } from "$lib/random"; + +interface IWagonServiceOptions { + scene: GameScene +} + +export class WagonService { + public wagon!: Wagon + public outFlags: Flag[] = [] + public nearFlags: Flag[] = [] + public routeService: RouteService + public scene: GameScene + + constructor({ scene }: IWagonServiceOptions) { + this.scene = scene + this.routeService = new RouteService({ scene }) + } + + public update() { + this.updateWagon() + this.updateFlags() + this.routeService.update() + } + + public initWagon({ x, y }: { x: number; y: number }) { + this.wagon = new Wagon({ scene: this.scene, x, y }) + + this.initOutFlags() + this.initNearFlags() + } + + public findRandomOutFlag() { + return this.outFlags[Math.floor(Math.random() * this.outFlags.length)] + } + + public findRandomNearFlag() { + return this.nearFlags[Math.floor(Math.random() * this.nearFlags.length)] + } + + private updateWagon() { + const collisionObjects = + this.scene.chunkNow?.objects.filter( + (obj) => obj.isOnWagonPath && obj.state !== "DESTROYED", + ) ?? [] + for (const collisionObject of collisionObjects) { + const isInArea = this.wagon.checkIfPointInCollisionArea({ + x: collisionObject.x, + y: collisionObject.y, + }) + if (isInArea) { + this.wagon.state = "WAITING" + this.wagon.speedPerSecond = 0 + return + } + } + + if (this.wagon.fuel <= 1) { + this.wagon.state = "WAITING" + this.wagon.speedPerSecond = 0 + return + } + + if (this.wagon.state === "WAITING") { + this.wagon.state = "IDLE" + } + if (this.wagon.state === "IDLE") { + const target = this.routeService.route?.getNextFlag() + if (target) { + this.wagon.target = target + this.wagon.state = "MOVING" + } + } + if (this.wagon.state === "MOVING") { + this.wagon.speedPerSecond = 0.5 + const isMoving = this.wagon.move() + + if (!isMoving) { + if ( + this.wagon.target instanceof Flag && + this.wagon.target.type === "WAGON_MOVEMENT" + ) { + this.routeService.route?.removeFlag(this.wagon.target) + this.wagon.target = undefined + this.wagon.state = "IDLE" + this.wagon.speedPerSecond = 0 + } + } + } + + this.wagon.live() + } + + private updateFlags() { + for (const flag of this.nearFlags) { + flag.x = this.wagon.x + flag.offsetX + flag.y = this.wagon.y + flag.offsetY + } + for (const flag of this.outFlags) { + flag.x = this.wagon.x + flag.offsetX + flag.y = this.wagon.y + flag.offsetY + } + } + + private initOutFlags(count = 20) { + for (let i = 0; i < count; i++) { + this.outFlags.push(this.generateRandomOutFlag()) + } + } + + private initNearFlags(count = 20) { + for (let i = 0; i < count; i++) { + this.nearFlags.push(this.generateRandomNearFlag()) + } + } + + private generateRandomOutFlag() { + const minOffsetX = 1800 + const minOffsetY = 1200 + + const offsetX = + getRandomInRange(minOffsetX, minOffsetX * 1.5) * getMinusOrPlus() + const offsetY = + getRandomInRange(minOffsetY, minOffsetY * 1.5) * getMinusOrPlus() + + return new Flag({ + scene: this.scene, + type: "OUT_OF_SCREEN", + x: this.wagon.x + offsetX, + y: this.wagon.y + offsetY, + offsetX, + offsetY, + }) + } + + private generateRandomNearFlag() { + const minRadius = 280 + const maxRadius = minRadius * 1.1 + + const angle = Math.random() * Math.PI * 2 + const radius = getRandomInRange(minRadius, maxRadius) + + const offsetX = Math.round(Math.cos(angle) * radius) + const offsetY = Math.round(Math.sin(angle) * radius) + + return new Flag({ + scene: this.scene, + type: "WAGON_NEAR_MOVEMENT", + x: this.wagon.x + offsetX, + y: this.wagon.y + offsetY, + offsetX, + offsetY, + }) + } +} diff --git a/src/lib/game/types.ts b/src/lib/game/types.ts new file mode 100644 index 00000000..f085d62c --- /dev/null +++ b/src/lib/game/types.ts @@ -0,0 +1,399 @@ +export interface TwitchAccessTokenResponse { + access_token: string + refresh_token: string + scope: string[] + expires_in: number + token_type: "bearer" +} + +export interface TwitchAccessToken { + userId: string + accessToken: string + refreshToken: string | null + scope: string[] + expiresIn: number | null + obtainmentTimestamp: number +} + +export interface IGameAction { + command: string + commandDescription: string +} + +export interface IGameActionResponse { + ok: boolean + message: string | null +} + +export type IGameSceneAction = + | "HELP" + | "GIFT" + | "TRADE" + | "DONATE" + | "REFUEL" + | "STEAL_FUEL" + | "CHOP" + | "MINE" + | "PLANT" + | "START_GROUP_BUILD" + | "DISBAND_GROUP" + | "JOIN_GROUP" + | "START_POLL" + | "VOTE" + | "START_CHANGING_SCENE" + | "START_RAID" + | "CREATE_NEW_PLAYER" + | "START_CREATING_NEW_ADVENTURE" + | "SHOW_MESSAGE" + | "GITHUB" + | "CREATE_IDEA" + +export type ItemType = "WOOD" | "STONE" | "AXE" | "PICKAXE" | "COIN" + +export interface IGameInventory { + id: string + objectId: string + items: IGameInventoryItem[] +} + +export interface IGameInventoryItem { + id: string + createdAt: Date + updatedAt: Date + inventoryId: string + type: ItemType + amount: number + durability: number +} + +export interface IGameSkill { + id: string + type: "WOODSMAN" | "MINER" + objectId: string | null + lvl: number + xp: number + xpNextLvl: number +} + +export interface IGameQuest { + id: string + type: "MAIN" | "SIDE" + title: string + description: string + tasks: IGameQuestTask[] + status: "INACTIVE" | "ACTIVE" | "FAILED" | "SUCCESS" + creatorId: string + conditions: { + chunks?: number + limitSeconds?: number + reward?: string + } +} + +export interface IGameQuestTask { + id: string + description: string + status: "INACTIVE" | "ACTIVE" | "FAILED" | "SUCCESS" + progressNow: number | boolean + progressToSuccess: number | boolean + updateProgress: IGameQuestTaskFunc + command?: string + action?: IGameAction +} + +export type IGameQuestTaskFunc = ( + progressToSuccess?: IGameQuestTask["progressToSuccess"], +) => Partial + +export interface IGameChunk { + id: string + title: string + type: "VILLAGE" | "FOREST" | "LAKE" + center: { + x: number + y: number + } + area: IGameObjectArea +} + +export type IGameChunkTheme = + | "GREEN" + | "TOXIC" + | "STONE" + | "TEAL" + | "BLUE" + | "VIOLET" + +export interface IGameVillageChunk extends IGameChunk {} + +export interface IGameForestChunk extends IGameChunk {} + +export interface IGameLakeChunk extends IGameChunk {} + +export interface IGameObject { + id: string + x: number + y: number + state: IGameObjectState + direction: IGameObjectDirection + entity: IGameObjectEntity + target: IGameObject | undefined + health: number + speedPerSecond: number + size: number +} + +export type IGameObjectState = + | "MOVING" + | "IDLE" + | "WAITING" + | "CHOPPING" + | "MINING" + | "DESTROYED" +export type IGameObjectEntity = + | "RABBIT" + | "WOLF" + | "PLAYER" + | "RAIDER" + | "TREE" + | "STONE" + | "WATER" + | "LAKE" + | "FLAG" + | "AREA" + | "TRADER" + | "COURIER" + | "FARMER" + | "MECHANIC" + | "WAGON" + | IGameObjectBuildingType +export type IGameObjectDirection = "LEFT" | "RIGHT" + +export interface WebSocketMessage { + id: string + event: + | "OBJECT_UPDATED" + | "RAID_STARTED" + | "GROUP_FORM_STARTED" + | "SCENE_CHANGING_STARTED" + | "COUNTDOWN_NEXT_WAVE_STARTED" + | "SCENE_CHANGED" + | "VOTING_FOR_NEW_MAIN_QUEST_STARTED" + | "MAIN_QUEST_STARTED" + | "SIDE_QUEST_STARTED" + | "TRADE_STARTED" + | "IDEA_CREATED" + object?: Partial +} + +export interface IGameObjectWagon extends IGameObject { + fuel: number + visibilityArea: { + startX: number + endX: number + startY: number + endY: number + } + cargoType: "CHEST" | undefined +} + +export type IGameObjectBuildingType = + | "CAMPFIRE" + | "WAREHOUSE" + | "WAGON_STOP" + | "STORE" + | "CONSTRUCTION_AREA" + +export interface IGameObjectBuilding extends IGameObject { + inventory: IGameInventory +} + +export interface IGameBuildingCampfire extends IGameObjectBuilding {} + +export interface IGameBuildingWarehouse extends IGameObjectBuilding {} + +export interface IGameBuildingStore extends IGameObjectBuilding {} + +export interface IGameBuildingWagonStop extends IGameObjectBuilding {} + +export interface IGameBuildingConstructionArea extends IGameObjectBuilding {} + +export interface IGameObjectFlag extends IGameObject { + type: + | "MOVEMENT" + | "WAGON_MOVEMENT" + | "WAGON_NEAR_MOVEMENT" + | "RESOURCE" + | "SPAWN_LEFT" + | "SPAWN_RIGHT" + | "OUT_OF_SCREEN" + | "TRADE_POINT" +} + +export interface IGameObjectWater extends IGameObject {} + +export interface IGameObjectLake extends IGameObject { + water: IGameObjectWater[] +} + +export interface IGameObjectArea extends IGameObject { + theme: IGameChunkTheme + area: { + startX: number + endX: number + startY: number + endY: number + } +} + +export interface IGameObjectTree extends IGameObject { + type: "1" | "2" | "3" | "4" | "5" + variant: IGameChunkTheme + resource: number + isReadyToChop: boolean +} + +export interface IGameObjectStone extends IGameObject { + type: "1" + resource: number +} + +export interface IGameObjectUnit extends IGameObject { + userName: string + coins: number + inventory: IGameInventory + visual: { + head: "1" + hairstyle: "BOLD" | "CLASSIC" | "COAL_LONG" | "ORANGE_WITH_BEARD" + top: + | "VIOLET_SHIRT" + | "BLACK_SHIRT" + | "GREEN_SHIRT" + | "BLUE_SHIRT" + | "DARK_SILVER_SHIRT" + } + dialogue: { + messages: { id: string; text: string }[] + } +} + +export interface IGameObjectTrader extends IGameObjectUnit {} + +export interface IGameObjectCourier extends IGameObjectUnit {} + +export interface IGameObjectFarmer extends IGameObjectUnit {} + +export interface IGameObjectMechanic extends IGameObjectUnit {} + +export interface IGameObjectPlayer extends IGameObjectUnit { + reputation: number + villainPoints: number + refuellerPoints: number + raiderPoints: number + skills: IGameSkill[] + lastActionAt: Date +} + +export interface IGameObjectRaider extends IGameObjectUnit {} + +export interface IGameObjectRabbit extends IGameObject {} + +export interface IGameObjectWolf extends IGameObject {} + +export interface ITradeOffer { + id: string + type: "BUY" | "SELL" + amount: number + unitPrice: number + item: ItemType + commandToTrade: string +} + +export interface IGameScript { + id: string + tasks: IGameTask[] + isInterruptible: boolean + live: () => void +} + +export interface IGameTask { + id: string + status: "IDLE" | "ACTIVE" | "DONE" + target?: IGameObject + live: () => void +} + +export interface IGameEvent { + id: string + title: string + description: string + type: WebSocketMessage["event"] + status: "STARTED" | "STOPPED" + endsAt: Date + poll?: IGamePoll + quest?: IGameQuest + offers?: ITradeOffer[] +} + +export interface IGamePoll { + status: "ACTIVE" | "SUCCESS" | "FINISHED" + id: string + action: IGameAction + votesToSuccess: number + votes: { id: string; userName: string }[] +} + +export type GameSceneType = "VILLAGE" | "DEFENCE" | "MOVING" + +export interface GetSceneResponse { + id: string + commands: string[] + chunk: IGameChunk | null + events: IGameEvent[] + group: IGameGroup + wagon: IGameObjectWagon + route: IGameRoute | null + warehouseItems: IGameInventoryItem[] | undefined +} + +export interface IGameGroup { + id: string + players: IGameObjectPlayer[] +} + +export interface IGameRoute { + startPoint: { x: number; y: number } + endPoint: { x: number; y: number } + chunks: IGameChunk[] +} + +export interface PlayerTitle { + title: string + type: + | "RICH" + | "FAMOUS" + | "VIEWER" + | "RAIDER" + | "VILLAIN" + | "REFUELLER" + | "WOODSMAN" + | "MINER" +} + +export type GraphicsContainerType = + | "INTERFACE" + | "PLAYER_IDLE" + | "PLAYER_COINS" + | "PLAYER_WOOD" + | "PLAYER_STONE" + | "PLAYER_AXE" + | "PLAYER_PICKAXE" + | "UNIT_TOP" + | "UNIT_HEAD" + | "UNIT_HAIR" + | "WAGON_WHEEL" + | "WAGON_ENGINE" + | "WAGON_ENGINE_CLOUD" + | "WAGON_CARGO" + | "WAGON_FUEL" + | "FIRE_PARTICLE" diff --git a/src/lib/game/utils/assetsManager.ts b/src/lib/game/utils/assetsManager.ts new file mode 100644 index 00000000..0113d46c --- /dev/null +++ b/src/lib/game/utils/assetsManager.ts @@ -0,0 +1,448 @@ +import { + AnimatedSprite, + type Application, + Assets, + Graphics, + Sprite, + Texture, + TilingSprite, +} from "pixi.js" +import { type IGameObjectUnit } from "$lib/game/types" +import campfireAnimation1 + from "$lib/assets/game/images/animation/campfire/1.png" +import campfireAnimation2 + from "$lib/assets/game/images/animation/campfire/2.png" +import campfireAnimation3 + from "$lib/assets/game/images/animation/campfire/3.png" +import campfireAnimation4 + from "$lib/assets/game/images/animation/campfire/4.png" +import campfireAnimation5 + from "$lib/assets/game/images/animation/campfire/5.png" +import fireParticle1 + from "$lib/assets/game/images/animation/campfire/particle-1.png" +import fireParticle2 + from "$lib/assets/game/images/animation/campfire/particle-2.png" +import fireParticle3 + from "$lib/assets/game/images/animation/campfire/particle-3.png" +import heroRight1 + from "$lib/assets/game/images/animation/hero-moving-right/1.png" +import heroRight2 + from "$lib/assets/game/images/animation/hero-moving-right/2.png" +import heroRight3 + from "$lib/assets/game/images/animation/hero-moving-right/3.png" +import heroRight4 + from "$lib/assets/game/images/animation/hero-moving-right/4.png" +import heroRight5 + from "$lib/assets/game/images/animation/hero-moving-right/5.png" +import heroRight6 + from "$lib/assets/game/images/animation/hero-moving-right/6.png" +import campfire1 from "$lib/assets/game/images/buildings/camp-fire-1.png" +import constructionArea1 + from "$lib/assets/game/images/buildings/construction-1.png" +import store1 from "$lib/assets/game/images/buildings/store.png" +import wagonStop1 from "$lib/assets/game/images/buildings/wagon-stop-1.png" +import warehouse1 from "$lib/assets/game/images/buildings/warehouse-1.png" +import coin1 from "$lib/assets/game/images/icons/coin-1.png" +import flag1 from "$lib/assets/game/images/objects/flag-1.png" +import flag2 from "$lib/assets/game/images/objects/flag-2.png" +import stone1 from "$lib/assets/game/images/objects/stone-1.png" +import tree1Blue from "$lib/assets/game/images/objects/tree/1/blue.png" +import tree1Green from "$lib/assets/game/images/objects/tree/1/green.png" +import tree1Stone from "$lib/assets/game/images/objects/tree/1/stone.png" +import tree1Teal from "$lib/assets/game/images/objects/tree/1/teal.png" +import tree1Toxic from "$lib/assets/game/images/objects/tree/1/toxic.png" +import tree1Violet from "$lib/assets/game/images/objects/tree/1/violet.png" +import tree2Blue from "$lib/assets/game/images/objects/tree/2/blue.png" +import tree2Green from "$lib/assets/game/images/objects/tree/2/green.png" +import tree2Stone from "$lib/assets/game/images/objects/tree/2/stone.png" +import tree2Teal from "$lib/assets/game/images/objects/tree/2/teal.png" +import tree2Toxic from "$lib/assets/game/images/objects/tree/2/toxic.png" +import tree2Violet from "$lib/assets/game/images/objects/tree/2/violet.png" +import tree3Blue from "$lib/assets/game/images/objects/tree/3/blue.png" +import tree3Green from "$lib/assets/game/images/objects/tree/3/green.png" +import tree3Stone from "$lib/assets/game/images/objects/tree/3/stone.png" +import tree3Teal from "$lib/assets/game/images/objects/tree/3/teal.png" +import tree3Toxic from "$lib/assets/game/images/objects/tree/3/toxic.png" +import tree3Violet from "$lib/assets/game/images/objects/tree/3/violet.png" +import tree4Blue from "$lib/assets/game/images/objects/tree/4/blue.png" +import tree4Green from "$lib/assets/game/images/objects/tree/4/green.png" +import tree4Stone from "$lib/assets/game/images/objects/tree/4/stone.png" +import tree4Teal from "$lib/assets/game/images/objects/tree/4/teal.png" +import tree4Toxic from "$lib/assets/game/images/objects/tree/4/toxic.png" +import tree4Violet from "$lib/assets/game/images/objects/tree/4/violet.png" +import tree5Blue from "$lib/assets/game/images/objects/tree/5/blue.png" +import tree5Green from "$lib/assets/game/images/objects/tree/5/green.png" +import tree5Stone from "$lib/assets/game/images/objects/tree/5/stone.png" +import tree5Teal from "$lib/assets/game/images/objects/tree/5/teal.png" +import tree5Toxic from "$lib/assets/game/images/objects/tree/5/toxic.png" +import tree5Violet from "$lib/assets/game/images/objects/tree/5/violet.png" +import unitHairClassicRight + from "$lib/assets/game/images/unit/hairstyle/hair-classic-right.png" +import unitHairCoalLongRight + from "$lib/assets/game/images/unit/hairstyle/hair-coal-long-right.png" +import unitHairOrangeWithBeard + from "$lib/assets/game/images/unit/hairstyle/orange-with-beard.png" +import unitHead1Right from "$lib/assets/game/images/unit/head/head-1-right.png" +import blackTopRight from "$lib/assets/game/images/unit/top/black-top-right.png" +import blueTopRight from "$lib/assets/game/images/unit/top/blue-top-right.png" +import darkSilverTopRight + from "$lib/assets/game/images/unit/top/dark-silver-top-right.png" +import greenTopRight from "$lib/assets/game/images/unit/top/green-top-right.png" +import violetTopRight + from "$lib/assets/game/images/unit/top/violet-top-right.png" +import chest1 from "$lib/assets/game/images/wagon/chest-1.png" +import wagonEngineCloud1 from "$lib/assets/game/images/wagon/clouds/1.png" +import wagonEngineCloud2 from "$lib/assets/game/images/wagon/clouds/2.png" +import wagonEngineCloud3 from "$lib/assets/game/images/wagon/clouds/3.png" +import wagonEngineCloud4 from "$lib/assets/game/images/wagon/clouds/4.png" +import wagonEngine1 from "$lib/assets/game/images/wagon/engine-1.png" +import wagonFuel1 from "$lib/assets/game/images/wagon/fuel-1.png" +import wagonFuel2 from "$lib/assets/game/images/wagon/fuel-2.png" +import wagonFuelContainer1 + from "$lib/assets/game/images/wagon/fuel-container.png" +import wagonBase1 from "$lib/assets/game/images/wagon/wagon-1.png" +import wagonBase2 from "$lib/assets/game/images/wagon/wagon-2.png" +import wagonWheel1 from "$lib/assets/game/images/wagon/wheel-1.png" +import water1 from "$lib/assets/game/images/water/1.png" +import water2 from "$lib/assets/game/images/water/2.png" +import water3 from "$lib/assets/game/images/water/3.png" +import water4 from "$lib/assets/game/images/water/4.png" +import wood1 from "$lib/assets/game/images/icons/items/wood.png" +import stoneRes1 from "$lib/assets/game/images/icons/items/stone.png" +import toolAxe1 from "$lib/assets/game/images/tools/axe1_64.png" +import toolPickaxe1 from "$lib/assets/game/images/tools/pickaxe1_64.png" +import rabbitLeft from "$lib/assets/game/images/creatures/rabbit1_left_64.png" +import rabbitRight from "$lib/assets/game/images/creatures/rabbit1_right_64.png" +import wolfLeft from "$lib/assets/game/images/creatures/wolf1_left_64.png" +import wolfRight from "$lib/assets/game/images/creatures/wolf1_right_64.png" +import { getRandomInRange } from "$lib/random"; + +export abstract class AssetsManager { + static assets = [ + { alias: "water1", src: water1 }, + { alias: "water2", src: water2 }, + { alias: "water3", src: water3 }, + { alias: "water4", src: water4 }, + { alias: "stone1", src: stone1 }, + { alias: "rabbitLeft", src: rabbitLeft }, + { alias: "rabbitRight", src: rabbitRight }, + { alias: "wolfLeft", src: wolfLeft }, + { alias: "wolfRight", src: wolfRight }, + { alias: "wood1", src: wood1 }, + { alias: "stoneRes1", src: stoneRes1 }, + { alias: "toolAxe1", src: toolAxe1 }, + { alias: "toolPickaxe1", src: toolPickaxe1 }, + { alias: "campfire1", src: campfire1 }, + { alias: "campfireAnimation1", src: campfireAnimation1 }, + { alias: "campfireAnimation2", src: campfireAnimation2 }, + { alias: "campfireAnimation3", src: campfireAnimation3 }, + { alias: "campfireAnimation4", src: campfireAnimation4 }, + { alias: "campfireAnimation5", src: campfireAnimation5 }, + { alias: "fireParticle1", src: fireParticle1 }, + { alias: "fireParticle2", src: fireParticle2 }, + { alias: "fireParticle3", src: fireParticle3 }, + { alias: "warehouse1", src: warehouse1 }, + { alias: "wagonStop1", src: wagonStop1 }, + { alias: "store1", src: store1 }, + { alias: "constructionArea1", src: constructionArea1 }, + { alias: "flag1", src: flag1 }, + { alias: "flag2", src: flag2 }, + { alias: "wagonWheel1", src: wagonWheel1 }, + { alias: "wagonBase1", src: wagonBase1 }, + { alias: "wagonBase2", src: wagonBase2 }, + { alias: "wagonEngine1", src: wagonEngine1 }, + { alias: "wagonEngineCloud1", src: wagonEngineCloud1 }, + { alias: "wagonEngineCloud2", src: wagonEngineCloud2 }, + { alias: "wagonEngineCloud3", src: wagonEngineCloud3 }, + { alias: "wagonEngineCloud4", src: wagonEngineCloud4 }, + { alias: "wagonFuelContainer1", src: wagonFuelContainer1 }, + { alias: "wagonFuel1", src: wagonFuel1 }, + { alias: "wagonFuel2", src: wagonFuel2 }, + { alias: "chest1", src: chest1 }, + { alias: "coin1", src: coin1 }, + ] + + static animationAssets = [ + { alias: "heroRight1", src: heroRight1 }, + { alias: "heroRight2", src: heroRight2 }, + { alias: "heroRight3", src: heroRight3 }, + { alias: "heroRight4", src: heroRight4 }, + { alias: "heroRight5", src: heroRight5 }, + { alias: "heroRight6", src: heroRight6 }, + ] + + static headAssets = [{ alias: "unitHead1", src: unitHead1Right }] + + static hairAssets = [ + { alias: "unitHairClassic", src: unitHairClassicRight }, + { alias: "unitHairCoalLong", src: unitHairCoalLongRight }, + { alias: "unitHairOrangeWithBeard", src: unitHairOrangeWithBeard }, + ] + + static topAssets = [ + { alias: "violetTop", src: violetTopRight }, + { alias: "blackTop", src: blackTopRight }, + { alias: "greenTop", src: greenTopRight }, + { alias: "blueTop", src: blueTopRight }, + { alias: "darkSilverTop", src: darkSilverTopRight }, + ] + + static treeAssets = [ + { alias: "tree1Green", src: tree1Green }, + { alias: "tree1Blue", src: tree1Blue }, + { alias: "tree1Stone", src: tree1Stone }, + { + alias: "tree1Teal", + src: tree1Teal, + }, + { + alias: "tree1Toxic", + src: tree1Toxic, + }, + { + alias: "tree1Violet", + src: tree1Violet, + }, + { alias: "tree2Green", src: tree2Green }, + { alias: "tree2Blue", src: tree2Blue }, + { alias: "tree2Stone", src: tree2Stone }, + { + alias: "tree2Teal", + src: tree2Teal, + }, + { + alias: "tree2Toxic", + src: tree2Toxic, + }, + { + alias: "tree2Violet", + src: tree2Violet, + }, + { alias: "tree3Green", src: tree3Green }, + { alias: "tree3Blue", src: tree3Blue }, + { alias: "tree3Stone", src: tree3Stone }, + { + alias: "tree3Teal", + src: tree3Teal, + }, + { + alias: "tree3Toxic", + src: tree3Toxic, + }, + { + alias: "tree3Violet", + src: tree3Violet, + }, + { alias: "tree4Green", src: tree4Green }, + { alias: "tree4Blue", src: tree4Blue }, + { alias: "tree4Stone", src: tree4Stone }, + { + alias: "tree4Teal", + src: tree4Teal, + }, + { + alias: "tree4Toxic", + src: tree4Toxic, + }, + { + alias: "tree4Violet", + src: tree4Violet, + }, + { alias: "tree5Green", src: tree5Green }, + { alias: "tree5Blue", src: tree5Blue }, + { alias: "tree5Stone", src: tree5Stone }, + { + alias: "tree5Teal", + src: tree5Teal, + }, + { + alias: "tree5Toxic", + src: tree5Toxic, + }, + { + alias: "tree5Violet", + src: tree5Violet, + }, + ] + + public static async init() { + await Assets.load(AssetsManager.assets) + await Assets.load(AssetsManager.animationAssets) + await Assets.load(AssetsManager.headAssets) + await Assets.load(AssetsManager.hairAssets) + await Assets.load(AssetsManager.topAssets) + await Assets.load(AssetsManager.treeAssets) + } + + public static getAnimatedSpriteHero(direction: IGameObjectUnit["direction"]) { + const images = [ + "heroRight1", + "heroRight2", + "heroRight3", + "heroRight4", + "heroRight5", + "heroRight6", + ] + const textureArray = [] + + for (const image of images) { + const texture = Texture.from(image) + textureArray.push(texture) + } + + const animation = new AnimatedSprite(textureArray) + animation.anchor.set(0.5, 1) + + if (direction === "LEFT") { + // Flip horizontally + animation.scale.x = -1 + } + + return animation + } + + public static getAnimatedSpriteCampfire() { + const images = [ + "campfireAnimation1", + "campfireAnimation2", + "campfireAnimation3", + "campfireAnimation4", + "campfireAnimation5", + ] + const textureArray = [] + + for (const image of images) { + const texture = Texture.from(image) + textureArray.push(texture) + } + + const animation = new AnimatedSprite(textureArray) + animation.anchor.set(0.5, 1) + + return animation + } + + public static getRandomSpriteForBackground(app: Application) { + const graphics1 = new Graphics() + graphics1.rect(0, 0, 64, 64).fill(0x0b8a8f) + const texture1 = app.renderer.generateTexture(graphics1) + + const sprite1 = Sprite.from(texture1) + const sprite2 = Sprite.from("backgroundTeal2") + const sprite3 = Sprite.from("backgroundTeal3") + const sprite4 = Sprite.from("backgroundTeal4") + const sprite5 = Sprite.from("backgroundTeal5") + + const random = getRandomInRange(1, 100) + if (random <= 55) { + return sprite1 + } + if (random <= 93) { + return sprite2 + } + if (random <= 96) { + return sprite5 + } + if (random <= 98) { + return sprite3 + } + return sprite4 + } + + public static getRandomSpriteForWater() { + const sprite1 = Sprite.from("water1") + const sprite2 = Sprite.from("water2") + const sprite3 = Sprite.from("water3") + const sprite4 = Sprite.from("water4") + + const random = getRandomInRange(1, 100) + if (random <= 70) { + return sprite2 + } + if (random <= 80) { + return sprite4 + } + if (random <= 98) { + return sprite1 + } + return sprite3 + } + + public static generateRandomGridBackground({ + app, + width, + height, + }: { + app: Application + width: number + height: number + }) { + const gridX = Math.ceil(width / 64) + const gridY = Math.floor(height / 64) + + const bg: Sprite[] = [] + + for (let i = 0; i < gridX; i++) { + for (let j = 0; j < gridY; j++) { + const background = AssetsManager.getRandomSpriteForBackground(app) + + background.x = i * 64 + background.y = j * 64 + + bg.push(background) + } + } + + return bg + } + + public static async generateAndSaveBackground(app: Application) { + const bg = AssetsManager.generateRandomGridBackground({ + width: 2560, + height: 1440, + app, + }) + app.stage.addChild(...bg) + + return AssetsManager.saveCanvasAsWebp(app) + } + + public static async getGeneratedBackgroundTilingSprite(app: Application) { + const bg = AssetsManager.generateRandomGridBackground({ + width: 2560, + height: 1440, + app, + }) + app.stage.addChild(...bg) + + const blob = await app.renderer.extract.image({ + target: app.stage, + format: "webp", + quality: 1, + }) + + app.stage.removeChild(...bg) + + return new TilingSprite({ + texture: Texture.from(blob), + }) + } + + public static async saveCanvasAsWebp( + app: Application, + imageName = "untitled", + ) { + const blob = await app.renderer.extract.image({ + target: app.stage, + format: "webp", + quality: 1, + }) + + const link = document.createElement("a") + link.href = blob.src + link.download = `${imageName}.webp` + link.click() + link.remove() + app.renderer.destroy() + } +} diff --git a/src/lib/game/utils/audioManager.ts b/src/lib/game/utils/audioManager.ts new file mode 100644 index 00000000..3fbf998e --- /dev/null +++ b/src/lib/game/utils/audioManager.ts @@ -0,0 +1,103 @@ +import { Howl } from "howler" +import chop1Audio from "$lib/assets/game/audio/chop-1.wav" +import fireBurn1Audio from "$lib/assets/game/audio/fire-1.wav" +import forest1Audio from "$lib/assets/game/audio/forest-1.mp3" +import handPunch1Audio from "$lib/assets/game/audio/hand-punch-1.wav" +import marchWithHorns1Audio + from "$lib/assets/game/audio/marching-with-horns-1.wav" +import mine1Audio from "$lib/assets/game/audio/mine-1.wav" +import wagon1Audio from "$lib/assets/game/audio/wagon-1.wav" +import yeah1Audio from "$lib/assets/game/audio/yeah-1.wav" + +type SoundName = + | "CHOP_HIT" + | "MINE_HIT" + | "HAND_HIT" + | "MARCHING_WITH_HORNS" + | "FOREST_BACKGROUND" + | "WAGON_MOVING" + | "FIRE_BURN" + | "YEAH" + +export class AudioManager { + private chop1 = new Howl({ + src: chop1Audio, + }) + private mine1 = new Howl({ + src: mine1Audio, + rate: 0.7, + volume: 0.4, + }) + private handPunch1 = new Howl({ + src: handPunch1Audio, + volume: 0.2, + rate: 0.8, + }) + private marchWithHorns1 = new Howl({ + src: marchWithHorns1Audio, + volume: 0.7, + }) + private forest1 = new Howl({ + src: forest1Audio, + loop: true, + volume: 0.5, + }) + private wagon1 = new Howl({ + src: wagon1Audio, + rate: 0.7, + volume: 0.08, + }) + private fireBurn1 = new Howl({ + src: fireBurn1Audio, + volume: 0.7, + }) + private yeah1 = new Howl({ + src: yeah1Audio, + volume: 0.8, + }) + + private findSound(name: SoundName): Howl[] { + switch (name) { + case "CHOP_HIT": + return [this.chop1] + case "MINE_HIT": + return [this.mine1] + case "HAND_HIT": + return [this.handPunch1] + case "MARCHING_WITH_HORNS": + return [this.marchWithHorns1] + case "WAGON_MOVING": + return [this.wagon1] + case "FIRE_BURN": + return [this.fireBurn1] + case "YEAH": + return [this.yeah1] + case "FOREST_BACKGROUND": + return [this.forest1] + default: + return [] + } + } + + private play(audios: Howl[]) { + if (!audios.length) { + return + } + + const randomAudio = audios[Math.floor(Math.random() * audios.length)] + if (randomAudio.playing()) { + return + } + + randomAudio.play() + } + + public playSound(name: SoundName) { + return this.play(this.findSound(name)) + } + + public destroy() { + this.fireBurn1.stop() + this.forest1.stop() + } +} diff --git a/src/lib/game/utils/generators/background.ts b/src/lib/game/utils/generators/background.ts new file mode 100644 index 00000000..4960c517 --- /dev/null +++ b/src/lib/game/utils/generators/background.ts @@ -0,0 +1,280 @@ +import { Application, Container, Graphics, Sprite, TilingSprite } from "pixi.js" +import { type IGameChunkTheme } from "$lib/game/types" +import { + BACKGROUND_TILE_1, + BACKGROUND_TILE_2, + BACKGROUND_TILE_3, + BACKGROUND_TILE_4, + BACKGROUND_TILE_5, +} from "./backgroundImages" +import { getRandomInRange } from "$lib/random"; + +interface Palette { + 93: string + 97: string + 103: string + 111: string + 115: string + 123: string + 136: string + 147: string + 149: string + 151: string + 153: string + 156: string + 168: string + 173: string + 203: string + 206: string + 209: string + 211: string + 213: string + 218: string +} + +export class BackgroundGenerator { + app: Application + palette!: Palette + mainColor1!: string + mainColor2!: string + accentColor1!: string + accentColor2!: string + accentColor3!: string + + constructor(app: Application) { + this.app = app + + // Default + this.changePaletteByTheme("GREEN") + this.changePalette() + } + + generate(imageArray: number[]) { + const imageWidth = Math.sqrt(imageArray.length) + + const graphics = new Graphics() + + let index = 0 + for (let y = 0; y < imageWidth; y++) { + for (let x = 0; x < imageWidth; x++) { + const grayscaleKey = imageArray[index] + + if (!(grayscaleKey in this.palette)) { + // Not found! + console.log("Not found key in Palette:", grayscaleKey) + continue + } + + const color = this.palette[grayscaleKey as keyof Palette] + + graphics.rect(x, y, 1, 1).fill(color) + index++ + } + } + + return this.app.renderer.generateTexture({ + target: graphics, + resolution: 4, + }) + } + + public changePaletteByTheme(theme: IGameChunkTheme) { + if (theme === "GREEN") { + this.mainColor1 = "0x239063" + this.mainColor2 = "0x1ebc73" + this.accentColor1 = "0x91db69" + this.accentColor2 = "0xcddf6c" + this.accentColor3 = "0x8fd3ff" + } + if (theme === "TOXIC") { + this.mainColor1 = "0xa2a947" + this.mainColor2 = "0xd5e04b" + this.accentColor1 = "0xf9c22b" + this.accentColor2 = "0xed8099" + this.accentColor3 = "0xb2ba90" + } + if (theme === "VIOLET") { + this.mainColor1 = "0x6b3e75" + this.mainColor2 = "0x905ea9" + this.accentColor1 = "0xfdcbb0" + this.accentColor2 = "0xfbb954" + this.accentColor3 = "0x8fd3ff" + } + if (theme === "BLUE") { + this.mainColor1 = "0x4d65b4" + this.mainColor2 = "0x4d9be6" + this.accentColor1 = "0xa884f3" + this.accentColor2 = "0xc7dcd0" + this.accentColor3 = "0x8ff8e2" + } + if (theme === "TEAL") { + this.mainColor1 = "0x0b5e65" + this.mainColor2 = "0x0b8a8f" + this.accentColor1 = "0xfdcbb0" + this.accentColor2 = "0xfbb954" + this.accentColor3 = "0xf9c22b" + } + if (theme === "STONE") { + this.mainColor1 = "0x374e4a" + this.mainColor2 = "0x547e64" + this.accentColor1 = "0xfca790" + this.accentColor2 = "0xeaaded" + this.accentColor3 = "0x8fd3ff" + } + + this.changePalette() + } + + changePalette() { + this.palette = { + 93: this.mainColor1, + 97: this.mainColor1, + 103: this.mainColor1, + 111: this.mainColor2, + 115: this.mainColor2, + 123: "0xcd683d", // brown + 136: "0xcd683d", // brown + 147: this.accentColor1, + 149: "0x92a984", // light neutral + 151: "0xe6904e", // orange + 153: this.accentColor1, + 156: this.accentColor1, + 168: "0xf68181", // bright pink + 173: "0xfbb954", // light orange + 203: "0x8fd3ff", // light blue + 206: this.accentColor2, + 209: "0xc7dcd0", // almost white + 211: this.accentColor3, + 213: this.accentColor3, + 218: "0xfbff86", // light yellow + } + } + + async getPixelsData() { + const canvas = document.createElement("CANVAS") as HTMLCanvasElement + const ctx = canvas.getContext("2d") + if (!ctx) { + return + } + + const app = new Application() + await app.init() + + const img = Sprite.from("backgroundMini") + app.stage.addChild(img) + + const blob = await app.renderer.extract.image({ + target: app.stage, + format: "webp", + quality: 1, + }) + + ctx.canvas.width = blob.width + ctx.canvas.height = blob.height + ctx.drawImage(blob, 0, 0) + + const imageData = ctx.getImageData( + 0, + 0, + ctx.canvas.width, + ctx.canvas.height, + ) + + const pixels: number[] = [] + + for (let y = 0; y < blob.width; y++) { + for (let x = 0; x < blob.width; x++) { + const [redIndex, greenIndex, blueIndex, _] = + this.getColorIndicesForCoord(x, y, ctx.canvas.width) + + const average = Math.round( + (imageData?.data[redIndex] + + imageData?.data[greenIndex] + + imageData?.data[blueIndex]) / + 3, + ) + + pixels.push(average) + } + } + + // Result: array + console.log(pixels) + + return imageData + } + + getColorIndicesForCoord(x: number, y: number, width: number) { + const red = y * (width * 4) + x * 4 + return [red, red + 1, red + 2, red + 3] + } + + public getGeneratedBackgroundTilingSprite() { + const bg = this.generateRandomGridBackground({ + width: 2000, + height: 1000, + }) + const container = new Container() + container.addChild(...bg) + + const texture = this.app.renderer.generateTexture(container) + container.destroy() + + return new TilingSprite({ + texture, + }) + } + + generateRandomGridBackground({ + width, + height, + }: { + width: number + height: number + }) { + const gridX = Math.ceil(width / 64) + const gridY = Math.floor(height / 64) + + const bg: Sprite[] = [] + + for (let i = 0; i < gridX; i++) { + for (let j = 0; j < gridY; j++) { + const background = this.getRandomSpriteForBackground() + + background.x = i * 64 + background.y = j * 64 + + bg.push(background) + } + } + + return bg + } + + getRandomSpriteForBackground() { + const tileArray = this.getTileByRandomChance() + const sprite = Sprite.from(this.generate(tileArray)) + sprite.scale = 4 + + return sprite + } + + getTileByRandomChance(): number[] { + const randomMax = 100 + const random = getRandomInRange(1, randomMax) + + if (random <= 55) { + return BACKGROUND_TILE_1 + } + if (random <= 93) { + return BACKGROUND_TILE_2 + } + if (random <= 96) { + return BACKGROUND_TILE_3 + } + if (random <= 98) { + return BACKGROUND_TILE_4 + } + return BACKGROUND_TILE_5 + } +} diff --git a/src/lib/game/utils/generators/backgroundImages.ts b/src/lib/game/utils/generators/backgroundImages.ts new file mode 100644 index 00000000..0b01ff37 --- /dev/null +++ b/src/lib/game/utils/generators/backgroundImages.ts @@ -0,0 +1,88 @@ +export const BACKGROUND_TILE_1 = [ + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, +] + +export const BACKGROUND_TILE_2 = [ + 93, 93, 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 93, 111, 93, + 93, 111, 93, 93, 93, 93, 111, 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 93, + 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 111, 93, 93, 93, 93, 93, 93, 93, + 111, 93, 93, 93, 93, 93, 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 93, 93, + 111, 93, 93, 93, 111, 93, 93, 93, 93, 111, 93, 111, 93, 93, 93, 93, 111, 93, + 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 93, 93, 111, 93, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 111, 93, 93, 111, 93, 93, 111, 93, 93, + 111, 93, 93, 93, 111, 93, 93, 111, 93, 93, 93, 93, 93, 111, 93, 93, 111, 93, + 93, 93, 111, 93, 93, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 111, 93, 93, + 93, 93, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 111, 93, 93, 93, 111, 93, + 93, 93, 93, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 111, 93, 93, 93, 111, + 93, 93, 93, 93, 111, 93, 111, 93, 93, 93, 93, 93, 93, 111, 93, 111, 93, 93, + 111, 93, 93, 93, 111, 93, 93, 93, 111, 93, 93, 111, 93, 93, 93, 93, 111, 93, + 93, 93, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 93, 93, 93, +] + +export const BACKGROUND_TILE_3 = [ + 97, 97, 97, 149, 149, 97, 97, 97, 149, 149, 97, 97, 97, 115, 97, 97, 97, 206, + 97, 149, 115, 115, 97, 115, 115, 97, 97, 97, 103, 115, 103, 97, 97, 206, 206, + 97, 115, 115, 97, 115, 97, 97, 153, 153, 97, 97, 103, 97, 103, 115, 115, 97, + 103, 115, 97, 97, 103, 97, 153, 115, 115, 97, 149, 149, 97, 103, 115, 97, 103, + 115, 103, 97, 97, 97, 103, 115, 115, 103, 115, 115, 97, 97, 97, 97, 97, 97, + 97, 97, 206, 206, 103, 97, 97, 103, 97, 115, 97, 206, 206, 206, 97, 97, 97, + 115, 115, 206, 97, 97, 97, 97, 103, 97, 97, 97, 206, 115, 115, 97, 103, 115, + 115, 103, 97, 149, 103, 206, 206, 97, 97, 97, 103, 115, 115, 97, 103, 115, + 103, 97, 149, 115, 103, 115, 115, 97, 153, 153, 97, 103, 115, 97, 103, 97, + 103, 97, 115, 103, 97, 103, 115, 97, 153, 115, 115, 97, 97, 103, 153, 153, 97, + 97, 97, 97, 97, 103, 97, 97, 103, 115, 115, 103, 97, 103, 153, 115, 115, 97, + 97, 206, 206, 206, 97, 97, 97, 103, 97, 103, 97, 97, 103, 115, 115, 97, 115, + 115, 206, 97, 97, 97, 149, 97, 97, 97, 206, 206, 97, 97, 97, 103, 115, 115, + 103, 97, 206, 97, 115, 149, 97, 97, 103, 115, 115, 103, 97, 103, 115, 103, 97, + 115, 206, 97, 97, 115, 103, 97, 97, 103, 115, 103, 97, 103, 97, 97, 97, 115, + 115, 103, +] + +export const BACKGROUND_TILE_4 = [ + 93, 93, 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 93, 111, 93, + 93, 111, 93, 93, 93, 93, 111, 93, 93, 111, 173, 93, 93, 93, 111, 93, 93, 93, + 93, 93, 111, 93, 93, 93, 93, 173, 168, 173, 111, 93, 93, 93, 93, 93, 93, 93, + 111, 93, 93, 93, 93, 103, 173, 93, 111, 93, 93, 93, 211, 111, 93, 93, 93, 93, + 111, 93, 93, 93, 111, 93, 93, 93, 93, 211, 168, 111, 93, 93, 93, 93, 111, 93, + 93, 93, 111, 93, 93, 93, 93, 103, 93, 103, 93, 93, 93, 209, 209, 93, 111, 93, + 93, 93, 93, 93, 93, 93, 93, 93, 93, 209, 173, 173, 173, 93, 111, 93, 93, 111, + 93, 93, 111, 93, 93, 93, 103, 209, 173, 136, 173, 209, 93, 93, 93, 111, 93, + 93, 111, 93, 93, 93, 103, 103, 173, 173, 173, 209, 93, 111, 93, 93, 93, 93, + 93, 93, 111, 93, 93, 103, 209, 209, 103, 93, 93, 111, 93, 93, 93, 93, 93, 93, + 111, 93, 93, 93, 111, 93, 93, 93, 93, 218, 93, 93, 93, 111, 93, 173, 93, 93, + 93, 93, 111, 93, 93, 93, 111, 147, 218, 93, 93, 111, 173, 136, 173, 93, 93, + 93, 93, 93, 111, 93, 103, 93, 93, 111, 93, 103, 103, 173, 93, 93, 93, 111, 93, + 93, 111, 93, 93, 93, 93, 111, 93, 103, 93, 93, 93, 93, 93, 111, 93, 93, 93, + 93, 93, 93, 93, 93, 93, +] + +export const BACKGROUND_TILE_5 = [ + 93, 93, 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 93, 111, 93, + 93, 111, 93, 211, 211, 93, 111, 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 211, + 173, 173, 173, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 103, 211, 173, + 123, 173, 211, 93, 93, 93, 93, 213, 93, 93, 93, 93, 93, 103, 103, 173, 173, + 173, 211, 111, 93, 93, 93, 151, 213, 93, 93, 93, 111, 93, 103, 211, 211, 156, + 93, 111, 93, 93, 103, 156, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 93, + 93, 111, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, 111, 93, 93, 111, 93, + 93, 111, 93, 93, 111, 203, 93, 93, 111, 93, 93, 111, 93, 93, 93, 93, 93, 111, + 93, 93, 203, 136, 203, 93, 111, 93, 93, 93, 93, 93, 93, 209, 93, 93, 93, 93, + 103, 156, 111, 93, 93, 93, 93, 93, 93, 93, 211, 209, 151, 93, 93, 93, 93, 93, + 111, 93, 93, 93, 111, 93, 93, 103, 209, 151, 209, 209, 93, 111, 93, 93, 93, + 93, 218, 93, 111, 93, 103, 103, 211, 209, 211, 93, 93, 111, 93, 111, 93, 218, + 168, 93, 93, 93, 111, 103, 103, 156, 103, 111, 93, 93, 93, 111, 93, 103, 156, + 111, 93, 93, 111, 93, 93, 93, 93, 111, 93, 93, 93, 93, 93, 93, 93, 93, 93, 93, + 93, 93, 93, 93, 93, 93, 93, +] diff --git a/src/lib/game/utils/index.ts b/src/lib/game/utils/index.ts new file mode 100644 index 00000000..e7325dc6 --- /dev/null +++ b/src/lib/game/utils/index.ts @@ -0,0 +1,3 @@ +export { AssetsManager } from "./assetsManager" +export { AudioManager } from "./audioManager" +export { WebSocketManager } from "./webSocketManager" diff --git a/src/lib/game/utils/messageController.ts b/src/lib/game/utils/messageController.ts new file mode 100644 index 00000000..f5116616 --- /dev/null +++ b/src/lib/game/utils/messageController.ts @@ -0,0 +1,12 @@ +import type { WebSocketMessage } from "$lib/game/types"; + +export class MessageController { + public static parse(message: string): WebSocketMessage | undefined { + const parsed = JSON.parse(message) + if (parsed) { + return parsed as WebSocketMessage + } + + return undefined + } +} diff --git a/src/lib/game/utils/webSocketManager.ts b/src/lib/game/utils/webSocketManager.ts new file mode 100644 index 00000000..9276ebe4 --- /dev/null +++ b/src/lib/game/utils/webSocketManager.ts @@ -0,0 +1,37 @@ +import type { Game } from "../game" +import { MessageController } from "$lib/game/utils/messageController"; + +export abstract class WebSocketManager { + public static socket: WebSocket + public static messagesPerSecond = 0 + public static kbitPerSecond = 0 + + public static init(game: Game) { + WebSocketManager.socket = new WebSocket("ws://localhost:4002") + + WebSocketManager.setMessagesPerSecondHandler() + + WebSocketManager.socket.addEventListener("message", (event) => { + const message = MessageController.parse(event.data.toString()) + if (!message) { + return + } + + WebSocketManager.messagesPerSecond += 1 + const bytes = JSON.stringify(message).length + WebSocketManager.kbitPerSecond += Math.round((bytes * 8) / 1024) + + game.handleMessage(message) + }) + } + + public static setMessagesPerSecondHandler() { + return setInterval(() => { + // console.log( + // `${WebSocketManager.messagesPerSecond} msg/s, ${WebSocketManager.kbitPerSecond} kbit/s`, + // ) + WebSocketManager.messagesPerSecond = 0 + WebSocketManager.kbitPerSecond = 0 + }, 1000) + } +} diff --git a/src/lib/random.ts b/src/lib/random.ts new file mode 100644 index 00000000..81a8fdea --- /dev/null +++ b/src/lib/random.ts @@ -0,0 +1,9 @@ +export function getRandomInRange(min: number, max: number): number { + const ceilMin = Math.ceil(min) + return Math.floor(Math.random() * (max - ceilMin + 1)) + ceilMin +} + +export function getMinusOrPlus(): number { + // -1 or 1 + return Math.round(Math.random()) * 2 - 1 +} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 8c3f207a..6fba8564 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,9 +1,30 @@ @@ -20,10 +41,8 @@ "!команды", которые запускают динамичные действия. -
-
- -
+
+
@@ -35,6 +54,16 @@
\ No newline at end of file diff --git a/src/routes/auth/sign-in/+page.svelte b/src/routes/auth/sign-in/+page.svelte index f9260ce9..ac676203 100644 --- a/src/routes/auth/sign-in/+page.svelte +++ b/src/routes/auth/sign-in/+page.svelte @@ -1,7 +1,8 @@