From afe203f28251229a155d7b5f7076d1aab057713b Mon Sep 17 00:00:00 2001 From: Iskandarov Timur Date: Tue, 15 Oct 2024 06:51:11 +0500 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=BA=D0=B8:=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D0=BD=D0=B5=D1=81=D0=BA=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE=20?= =?UTF-8?q?=D1=82=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=BF=D1=80=D0=B5=D0=BF=D1=8F?= =?UTF-8?q?=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B9=20-=20=D0=94=D0=BE=D0=B1?= =?UTF-8?q?=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF=D1=80=D0=B5=D0=BF=D1=8F=D1=82?= =?UTF-8?q?=D1=81=D1=82=D0=B2=D0=B8=D1=8F=D0=BC=20=D0=B7=D0=B4=D0=BE=D1=80?= =?UTF-8?q?=D0=BE=D0=B2=D1=8C=D0=B5=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=20=D1=81=D0=BC=D0=B5=D0=BD=D1=8F=D0=B5=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D1=8C=20=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D0=B2=D0=B8=D0=B4=D0=B0=20=D0=B4=D0=BB=D1=8F=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D1=80=D1=83=D1=88=D0=B0=D0=B5=D0=BC=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=20=D1=8D=D1=84=D1=84=D0=B5=D0=BA=D1=82=D1=8B=20=D0=B2=D1=8B?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=B5=D0=BB=D0=BE=D0=B2=20=D0=B8=20=D0=BF?= =?UTF-8?q?=D0=BE=D0=BF=D0=BE=D0=B4=D0=B0=D0=BD=D0=B8=D0=B9=20-=20=D0=94?= =?UTF-8?q?=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B2=D0=BE=D0=B7=D0=BC?= =?UTF-8?q?=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=83=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=D0=B0=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B8=D0=B7?= =?UTF-8?q?=D0=B8=D0=B9=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=BF=D1=8F=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/assets/images/sprites/bang.png | Bin 0 -> 13272 bytes .../src/assets/images/sprites/enemy.svg | 14 -- .../client/src/assets/images/sprites/shot.png | Bin 0 -> 7644 bytes .../src/assets/images/sprites/steel.png | Bin 0 -> 1060 bytes .../client/src/assets/images/sprites/tank.svg | 14 -- .../client/src/assets/images/sprites/tree.png | Bin 0 -> 2857 bytes .../client/src/assets/images/sprites/wall.png | Bin 0 -> 4093 bytes .../client/src/assets/images/sprites/wall.svg | 9 - packages/client/src/components/Game/Game.tsx | 4 +- .../client/src/components/Game/bullet.tsx | 24 +- .../client/src/components/Game/collision.tsx | 3 +- .../client/src/components/Game/effects.tsx | 74 ++++++ packages/client/src/components/Game/enemy.tsx | 21 +- .../client/src/components/Game/gameLoop.tsx | 19 +- .../client/src/components/Game/gameTypes.tsx | 25 ++- .../client/src/components/Game/obstacle.tsx | 212 +++++++++++++++--- .../client/src/components/Game/player.tsx | 2 +- packages/client/src/components/Game/utils.tsx | 158 +++++++++++-- 18 files changed, 467 insertions(+), 112 deletions(-) create mode 100644 packages/client/src/assets/images/sprites/bang.png delete mode 100644 packages/client/src/assets/images/sprites/enemy.svg create mode 100644 packages/client/src/assets/images/sprites/shot.png create mode 100644 packages/client/src/assets/images/sprites/steel.png delete mode 100644 packages/client/src/assets/images/sprites/tank.svg create mode 100644 packages/client/src/assets/images/sprites/tree.png create mode 100644 packages/client/src/assets/images/sprites/wall.png delete mode 100644 packages/client/src/assets/images/sprites/wall.svg create mode 100644 packages/client/src/components/Game/effects.tsx diff --git a/packages/client/src/assets/images/sprites/bang.png b/packages/client/src/assets/images/sprites/bang.png new file mode 100644 index 0000000000000000000000000000000000000000..b220ec0b6330f5bf60c081f69b6510daa4c874c4 GIT binary patch literal 13272 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 Wzf=V+vdk#}0000 - - - - - - - - - - - - - diff --git a/packages/client/src/assets/images/sprites/shot.png b/packages/client/src/assets/images/sprites/shot.png new file mode 100644 index 0000000000000000000000000000000000000000..991d07e258720f6ea3bd6caa998bad6e4c45552c GIT binary patch literal 7644 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 KMNUMnLSTZGPGDaE literal 0 HcmV?d00001 diff --git a/packages/client/src/assets/images/sprites/steel.png b/packages/client/src/assets/images/sprites/steel.png new file mode 100644 index 0000000000000000000000000000000000000000..ad85f90864d0620e1c7d2cbb8b8fa4dd1dcf9adc GIT binary patch literal 1060 zcmV+<1l#+GP)N2bPDNB8 zb~7$DE-^7j^FlWO00W>&L_t(oN6nb&P7_fSfPE7&V9PFRp-@XHB!U(YBdffEY=u&m zLTT8Sf}jZzfzl0P)ITnWgczk&WAv8`7~l=$8IJc%yyKmAXlJIx^5aW7bMHCx_0C=A zLgK{)I8SgwNeP!AJvoJt8aL2^7g8rCfwj*Diw~CK!oZ*%GE&q238{!sW78e54Gn{T z^YMSf?e&9WWDJ^`TOd6pHC(92+6TW+PT=tPIGm#+__==|;P6PKUI_aKzrx}u-@gBV zVaEvf>{Ep>iF1BI!0g;T7$B|J)zi!47#$C*7s}}Pq#!1g=P$Ql&^|0psJ^Zq8tNO* zT&A}67F3z4z-+O~6Dqk{0)`U9nYT=-p%k=*TIlGq$P*fym;wvAuDDhSg_;5mZGo1E z>La7mgH}_>$FP(e%c0%e4R_l+IXTHf>}$eAWyUgSCrM(VvZ4}lva$uOB~6HoCiXR< zkm|AueEtmxe*DDqJ`jL+djSXpKM3w4d%+-AMtSn|8K~9C%7kz@vO)NoP{_Zr$@dZL zM_ATp8-f|qn)E(0J~ac~BoCY8(W(nnDs`C9i&w8<$nKCXgb&7k!3qgvobyp-5HTS; znSXY6-%1yvqlpz#B7=kqp$x`I7NRl;6>@vLTr#;b$g0cD3sD(Fk`OKj?8B(gy4#~j zW@KyvJU%}sRFIc1h(#gE^ALp!d3`=f9he1WupTdzui<0yk8N$gQ7*)eGDND(#PlqA zW$OYVl<6H9ls8jT#DwI?SbF>UPK`oT2J4-PkX#wOM};)GdAtmwOo*&aR7i;ovU1AA zC|d|uZCof($m8`XlEL3DGD2e(qFV^Mmf^5xld(jSnI!k+@x>eZ?94r;1uh}4`sAzkrR(36u#XktXE z4AwP~Bap6G$6d=3!gCW6nmiU61P>7OI;xPFoR#o+i}QmRxCh20EVzXbt4qP59U4RV z{N-z8p@xPg=;$y*YiqkGZEZrt>ac{?7o)kkMO>)5`X)J58smb=R4o+3k44F3@{1>e e4_4=g1o#8?_f=)ateN=$0000 - - - - - - - - - - - - - diff --git a/packages/client/src/assets/images/sprites/tree.png b/packages/client/src/assets/images/sprites/tree.png new file mode 100644 index 0000000000000000000000000000000000000000..51bd31a16f2faefa39a0e14246a8bcfbb44b45df GIT binary patch literal 2857 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 - - - - - - - - diff --git a/packages/client/src/components/Game/Game.tsx b/packages/client/src/components/Game/Game.tsx index ddef2c4..abedf67 100644 --- a/packages/client/src/components/Game/Game.tsx +++ b/packages/client/src/components/Game/Game.tsx @@ -7,7 +7,7 @@ import { import { PLAYER_DEFAULT_PARAMS } from '@/components/Game/player' import { gameLoop } from '@/components/Game/gameLoop' import { handleKeyDown, handleKeyUp } from '@/components/Game/controls' -import { AbstractEntity, Obstacle } from '@/components/Game/gameTypes' +import { AbstractEntity, Effect, Obstacle } from '@/components/Game/gameTypes' import { initializeCompanyMapObstacle, initializeRandomObstacle, @@ -22,6 +22,7 @@ export const Game: React.FC = () => { const enemiesRef = useRef(initializeRandomEnemies(5)) const bulletsRef = useRef([]) const obstaclesRef = useRef(initializeRandomObstacle(20)) + const effectsRef = useRef([]) const livesRef = useRef(livesUse) const [gameStarted, setGameStarted] = useState(false) const [isPaused, setIsPaused] = useState(false) @@ -50,6 +51,7 @@ export const Game: React.FC = () => { enemiesRef, bulletsRef, obstaclesRef, + effectsRef, livesRef, handleGameOver ) diff --git a/packages/client/src/components/Game/bullet.tsx b/packages/client/src/components/Game/bullet.tsx index 2a69dc4..e527003 100644 --- a/packages/client/src/components/Game/bullet.tsx +++ b/packages/client/src/components/Game/bullet.tsx @@ -1,4 +1,10 @@ import { AbstractEntity } from '@/components/Game/gameTypes' +import { createShotEffect } from './effects' + +const bulletSize = { + width: 12, + height: 18, +} export const createBullet = (enemy: AbstractEntity): AbstractEntity => { const bulletSpeed = 5 // Задайте скорость пули @@ -10,29 +16,31 @@ export const createBullet = (enemy: AbstractEntity): AbstractEntity => { if (enemy.direction.y < 0) { // Вверх bulletX = enemy.x + enemy.width / 2 // Центр по X - bulletY = enemy.y // Верхняя часть врага + bulletY = enemy.y - bulletSize.height // Верхняя часть врага } else if (enemy.direction.y > 0) { // Вниз bulletX = enemy.x + enemy.width / 2 // Центр по X - bulletY = enemy.y + enemy.height // Нижняя часть врага + bulletY = enemy.y + enemy.height + bulletSize.height // Нижняя часть врага } else if (enemy.direction.x < 0) { // Влево - bulletX = enemy.x // Левый край врага + bulletX = enemy.x - bulletSize.height // Левый край врага bulletY = enemy.y + enemy.height / 2 // Центр по Y } else if (enemy.direction.x > 0) { // Вправо - bulletX = enemy.x + enemy.width // Правый край врага + bulletX = enemy.x + enemy.width + bulletSize.height // Правый край врага bulletY = enemy.y + enemy.height / 2 // Центр по Y } else { bulletX = enemy.x + enemy.width / 2 // По умолчанию - центр bulletY = enemy.y + enemy.height / 2 // По умолчанию - центр } + createShotEffect(bulletX, bulletY, bulletDirection) + return { - x: bulletX, - y: bulletY, - width: 5, // Ширина пули - height: 5, // Высота пули + x: bulletX - bulletSize.width / 2, + y: bulletY - bulletSize.height / 2, + width: bulletSize.width, // Ширина пули + height: bulletSize.height, // Высота пули speed: bulletSpeed, direction: bulletDirection, } diff --git a/packages/client/src/components/Game/collision.tsx b/packages/client/src/components/Game/collision.tsx index aa62ce3..1b7fd49 100644 --- a/packages/client/src/components/Game/collision.tsx +++ b/packages/client/src/components/Game/collision.tsx @@ -8,7 +8,8 @@ export const detectCollision = ( player.x < obstacle.x + obstacle.width && player.x + player.width > obstacle.x && player.y < obstacle.y + obstacle.height && - player.y + player.height > obstacle.y + player.y + player.height > obstacle.y && + obstacle.isCollide ) } diff --git a/packages/client/src/components/Game/effects.tsx b/packages/client/src/components/Game/effects.tsx new file mode 100644 index 0000000..c9d67ed --- /dev/null +++ b/packages/client/src/components/Game/effects.tsx @@ -0,0 +1,74 @@ +import { Direction, Effect } from '@/components/Game/gameTypes' +import { MutableRefObject } from 'react' + +interface EffectSettings { + [key: string]: { + width: number + height: number + animation: Effect['animation'] + } +} + +let effectsRef: MutableRefObject + +const effectSettings: EffectSettings = { + bang: { + width: 60, + height: 60, + animation: { + frameInterval: 5, + frameCount: 0, + totalFrames: 8, + currentFrame: 0, + }, + }, + shot: { + width: 40, + height: 40, + animation: { + frameInterval: 5, + frameCount: 0, + totalFrames: 4, + currentFrame: 0, + }, + }, +} + +export const initEffects = (effects: MutableRefObject) => { + effectsRef = effects +} + +export const createEffect = ( + type: string, + x: number, + y: number, + direction?: Direction +) => { + if (effectsRef) { + effectsRef.current.push({ + type, + x: x - effectSettings[type].width / 2, + y: y - effectSettings[type].height / 2, + width: effectSettings[type].width, + height: effectSettings[type].height, + direction, + animation: { ...effectSettings[type].animation }, + }) + } +} + +export const createBangEffect = (x: number, y: number) => { + createEffect('bang', x, y) +} + +export const createShotEffect = ( + x: number, + y: number, + direction: Direction +) => { + createEffect('shot', x, y, direction) +} + +export const deleteEffect = (effect: Effect) => { + effectsRef.current = effectsRef.current.filter(i => i !== effect) +} diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index 20b2526..7c8a3fc 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -1,7 +1,10 @@ import { getRandomEdgePosition } from './utils' import { AbstractEntity, Enemy, Obstacle } from '@/components/Game/gameTypes' import { createBullet } from '@/components/Game/bullet' -import { detectCollision } from '@/components/Game/collision' +import { + detectCollision, + detectEnemyCollision, +} from '@/components/Game/collision' const enemyParams = { width: 70, @@ -37,8 +40,8 @@ export const initializeCampanyEnemies = (): Enemy[] => { { ...enemyParams, id: 0, - x: 50, - y: 55, + x: 360, + y: 0, animation: { currentFrame: 0, totalFrames: 4, @@ -49,8 +52,8 @@ export const initializeCampanyEnemies = (): Enemy[] => { { ...enemyParams, id: 1, - x: 320, - y: 250, + x: 0, + y: 108, animation: { currentFrame: 0, totalFrames: 4, @@ -61,8 +64,8 @@ export const initializeCampanyEnemies = (): Enemy[] => { { ...enemyParams, id: 2, - x: 715, - y: 60, + x: 720, + y: 108, animation: { currentFrame: 0, totalFrames: 4, @@ -119,7 +122,7 @@ export const updateEnemyPositions = ( // Проверка столкновений с другими врагами const hasEnemyCollision = enemiesRef.current.some(otherEnemy => { if (otherEnemy === enemy) return false - return detectCollision({ ...enemy, x: newX, y: newY }, otherEnemy) + return detectEnemyCollision({ ...enemy, x: newX, y: newY }, otherEnemy) }) // Проверка коллизий с препятствиями @@ -143,7 +146,7 @@ const isPositionOccupied = ( enemies: AbstractEntity[] ) => { return enemies.some(enemy => - detectCollision({ ...enemy, x: position.x, y: position.y }, enemy) + detectEnemyCollision({ ...enemy, x: position.x, y: position.y }, enemy) ) } diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index 5061a44..e1c8833 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -6,12 +6,14 @@ import { drawEnemies, drawObstacles, drawBullets, + drawEffects, } from './utils' import { ControlsProps, AbstractEntity, Obstacle, Enemy, + Effect, } from '@/components/Game/gameTypes' import { detectBulletCollision, @@ -20,6 +22,7 @@ import { import { updatePlayerAction } from '@/components/Game/controls' import { updateBullets } from '@/components/Game/bullet' import { handleBulletObstacleCollisions } from '@/components/Game/obstacle' +import { createBangEffect, initEffects } from './effects' /** * Основной игровой цикл, который обновляет состояние игры и перерисовывает экран каждый кадр. @@ -39,6 +42,7 @@ export const gameLoop = ( enemiesRef: React.MutableRefObject, bulletsRef: React.MutableRefObject, obstaclesRef: React.MutableRefObject, + effectsRef: React.MutableRefObject, livesRef: React.MutableRefObject, handleGameOver: () => void ) => { @@ -62,6 +66,8 @@ export const gameLoop = ( // Обработка столкновений с препятствиями handleBulletObstacleCollisions(bulletsRef.current, obstaclesRef.current) + initEffects(effectsRef) + bulletsRef.current = updateBullets( bulletsRef.current, canvasRef.current.width, @@ -69,9 +75,10 @@ export const gameLoop = ( ) // Отрисовка всех игровых объектов - drawObstacles(context, obstaclesRef.current) drawPlayer(context, playerRef.current) drawEnemies(context, enemiesRef.current) + drawObstacles(context, obstaclesRef.current) + drawEffects(context, effectsRef.current) drawBullets(context, bulletsRef.current) // Отрисовка пуль // Проверка на столкновения пуль с врагами @@ -81,6 +88,11 @@ export const gameLoop = ( if (hit) { // Убираем врага, если попали killEnemy(enemiesRef, enemy) + // Эффект поподания + createBangEffect( + bullet.x + bullet.width / 2, + bullet.y + bullet.height / 2 + ) // Убираем пулю, если попали bulletsRef.current = bulletsRef.current.filter(b => b !== bullet) return false @@ -90,6 +102,11 @@ export const gameLoop = ( if (detectBulletCollision(bullet, playerRef.current)) { // Уменьшаем жизни игрока livesRef.current -= 1 + // Эффект поподания + createBangEffect( + bullet.x + bullet.width / 2, + bullet.y + bullet.height / 2 + ) // Удаляем пулю после попадания bulletsRef.current = bulletsRef.current.filter(b => b !== bullet) // Проверка на окончание игры diff --git a/packages/client/src/components/Game/gameTypes.tsx b/packages/client/src/components/Game/gameTypes.tsx index 7978ee5..b28238a 100644 --- a/packages/client/src/components/Game/gameTypes.tsx +++ b/packages/client/src/components/Game/gameTypes.tsx @@ -1,18 +1,23 @@ interface AnimationParams { currentFrame: number totalFrames: number - frameInterval: number + frameInterval?: number frameCount?: number } +export interface Direction { + x: number + y: number +} + export interface AbstractEntity { x: number y: number width: number height: number speed: number - direction: { x: number; y: number } - animation: AnimationParams + direction: Direction + animation?: AnimationParams } export interface Enemy extends AbstractEntity { @@ -21,10 +26,24 @@ export interface Enemy extends AbstractEntity { } export interface Obstacle { + type: string x: number y: number width: number height: number + hp: number + isCollide: boolean + animation: AnimationParams +} + +export interface Effect { + type: string + x: number + y: number + width: number + height: number + direction?: Direction + animation: AnimationParams } export interface ControlsProps { diff --git a/packages/client/src/components/Game/obstacle.tsx b/packages/client/src/components/Game/obstacle.tsx index e1e9d7e..64314ab 100644 --- a/packages/client/src/components/Game/obstacle.tsx +++ b/packages/client/src/components/Game/obstacle.tsx @@ -4,35 +4,164 @@ import { detectCollision, detectObstacleCollision, } from '@/components/Game/collision' +import { createBangEffect } from './effects' + +enum Types { + Wall = 'wall', + Steel = 'steel', + Tree = 'tree', +} + +export const OBSTACLE_SIZE = 36 + +const ObstacleSettings = { + [Types.Wall]: { + hp: 2, + isCollide: true, + frames: [ + { + hp: 1, + index: 1, + }, + { + hp: 2, + index: 0, + }, + ], + }, + [Types.Steel]: { + hp: 1000, + isCollide: true, + frames: [ + { + hp: 1000, + index: 0, + }, + ], + }, + [Types.Tree]: { + hp: 1000, + isCollide: false, + frames: [ + { + hp: 1000, + index: 0, + }, + ], + }, +} export const initializeCompanyMapObstacle = (): Obstacle[] => { - return [ - { x: 0, y: 0, width: 120, height: 50 }, - { x: 200, y: 0, width: 70, height: 50 }, - { x: 350, y: 0, width: 70, height: 50 }, - { x: 500, y: 0, width: 120, height: 50 }, - { x: 700, y: 0, width: 100, height: 50 }, - - { x: 0, y: 130, width: 50, height: 120 }, - { x: 130, y: 130, width: 50, height: 70 }, - { x: 260, y: 130, width: 50, height: 200 }, - { x: 390, y: 130, width: 50, height: 70 }, - { x: 520, y: 130, width: 50, height: 120 }, - { x: 650, y: 130, width: 50, height: 125 }, - - { x: 0, y: 330, width: 120, height: 50 }, - { x: 200, y: 330, width: 150, height: 50 }, - { x: 350, y: 330, width: 70, height: 50 }, - { x: 500, y: 330, width: 120, height: 50 }, - { x: 700, y: 330, width: 100, height: 100 }, - - { x: 0, y: 460, width: 50, height: 140 }, - { x: 130, y: 530, width: 50, height: 70 }, - { x: 260, y: 480, width: 50, height: 120 }, - { x: 390, y: 460, width: 50, height: 70 }, - { x: 520, y: 490, width: 50, height: 120 }, - { x: 650, y: 480, width: 50, height: 120 }, - ] + return createMap([ + { type: Types.Steel, x: 72, y: 108, width: 72, height: 72 }, + { type: Types.Steel, x: 648, y: 108, width: 72, height: 72 }, + + { type: Types.Steel, x: 144, y: 360, width: 72, height: 72 }, + { type: Types.Steel, x: 576, y: 360, width: 72, height: 72 }, + { type: Types.Steel, x: 360, y: 360, width: 72, height: 72 }, + + { type: Types.Wall, x: 252, y: 72, width: 108, height: 36 }, + { type: Types.Wall, x: 324, y: 108, width: 36, height: 144 }, + { type: Types.Wall, x: 252, y: 216, width: 72, height: 36 }, + { type: Types.Wall, x: 252, y: 144, width: 72, height: 36 }, + { type: Types.Wall, x: 252, y: 108, width: 36, height: 36 }, + + { type: Types.Wall, x: 432, y: 72, width: 108, height: 36 }, + { type: Types.Wall, x: 504, y: 108, width: 36, height: 108 }, + { type: Types.Wall, x: 432, y: 108, width: 36, height: 108 }, + { type: Types.Wall, x: 432, y: 216, width: 108, height: 36 }, + + { type: Types.Wall, x: 72, y: 324, width: 72, height: 180 }, + { type: Types.Wall, x: 216, y: 324, width: 72, height: 180 }, + { type: Types.Wall, x: 504, y: 324, width: 72, height: 180 }, + { type: Types.Wall, x: 648, y: 324, width: 72, height: 180 }, + + { type: Types.Tree, x: 0, y: 324, width: 72, height: 180 }, + { type: Types.Tree, x: 720, y: 324, width: 72, height: 180 }, + { type: Types.Tree, x: 0, y: 504, width: 180, height: 72 }, + { type: Types.Tree, x: 612, y: 504, width: 180, height: 72 }, + ]) + return createMap([ + // /{ type: Types.Steel, x: 0, y: 0, width: 120, height: 50 }, + { type: Types.Steel, x: 200, y: 0, width: 70, height: 36 }, + { type: Types.Steel, x: 350, y: 0, width: 70, height: 36 }, + { type: Types.Steel, x: 500, y: 0, width: 120, height: 36 }, + // { type: Types.Wall, x: 700, y: 0, width: 100, height: 50 }, + + // { type: Types.Wall, x: 0, y: 130, width: 50, height: 120 }, + { type: Types.Wall, x: 130, y: 130, width: 50, height: 70 }, + // { type: Types.Wall, x: 260, y: 130, width: 50, height: 200 }, + { type: Types.Wall, x: 390, y: 130, width: 50, height: 70 }, + { type: Types.Wall, x: 520, y: 130, width: 50, height: 120 }, + // { type: Types.Wall, x: 650, y: 130, width: 50, height: 125 }, + + // { type: Types.Wall, x: 0, y: 330, width: 120, height: 50 }, + { type: Types.Wall, x: 200, y: 330, width: 150, height: 36 }, + { type: Types.Wall, x: 350, y: 330, width: 70, height: 36 }, + { type: Types.Wall, x: 500, y: 330, width: 120, height: 36 }, + // { type: Types.Wall, x: 700, y: 330, width: 100, height: 100 }, + + { type: Types.Wall, x: 0, y: 460, width: 50, height: 140 }, + { type: Types.Wall, x: 130, y: 530, width: 50, height: 70 }, + { type: Types.Wall, x: 260, y: 480, width: 50, height: 120 }, + //{ type: Types.Wall, x: 390, y: 460, width: 50, height: 70 }, + { type: Types.Wall, x: 520, y: 490, width: 50, height: 120 }, + { type: Types.Wall, x: 650, y: 480, width: 50, height: 120 }, + ]) +} + +export const createMap = ( + params: { + type: Types + x: number + y: number + width: number + height: number + }[] +) => { + let obstacles: Obstacle[] = [] + + params.forEach(obstacle => { + const { type, x, y, width, height } = obstacle + obstacles = [...obstacles, ...createObstacle(type, x, y, width, height)] + }) + + return obstacles +} + +export const createObstacle = ( + type: Types, + x: number, + y: number, + width: number, + height: number +): Obstacle[] => { + const obstacles: Obstacle[] = [] + const horizontalCount = Math.ceil(width / OBSTACLE_SIZE) + const verticalCount = Math.ceil(height / OBSTACLE_SIZE) + + Array.from({ length: horizontalCount }).forEach((_, i) => { + Array.from({ length: verticalCount }).forEach((_, j) => { + const obstacleX = x + i * OBSTACLE_SIZE + const obstacleY = y + j * OBSTACLE_SIZE + + obstacles.push({ + type, + x: obstacleX, + y: obstacleY, + width: OBSTACLE_SIZE, + height: OBSTACLE_SIZE, + hp: ObstacleSettings[type].hp, + isCollide: ObstacleSettings[type].isCollide, + animation: { + currentFrame: 0, + totalFrames: ObstacleSettings[type].frames.length, + }, + }) + }) + }) + + return obstacles } export const initializeRandomObstacle = ( @@ -42,7 +171,13 @@ export const initializeRandomObstacle = ( while (obstacles.length < numberOfObstacles) { const { x, y } = getRandomPosition(800, 600) - const obstacle: Obstacle = { x, y, width: 50, height: 50 } + const obstacle: Obstacle = createObstacle( + Types.Wall, + x, + y, + OBSTACLE_SIZE, + OBSTACLE_SIZE + )[0] // Проверяем, нет ли коллизий с существующими препятствиями const hasCollision = obstacles.some(existingObstacle => @@ -67,6 +202,10 @@ export const handleBulletObstacleCollisions = ( if (detectCollision(bullet, obstacle)) { // Логика для уничтожения пули, если она попала в препятствие bullets.splice(bullets.indexOf(bullet), 1) // Пример, удаляем пулю, если попала в препятствие + createBangEffect( + bullet.x + bullet.width / 2, + bullet.y + bullet.height / 2 + ) killObstacle(obstacles, obstacle) } }) @@ -74,5 +213,20 @@ export const handleBulletObstacleCollisions = ( } const killObstacle = (obstacles: Obstacle[], obstacle: Obstacle) => { - obstacles.splice(obstacles.indexOf(obstacle), 1) + const settings = ObstacleSettings[obstacle.type as Types] + const { frames } = settings + + obstacle.hp = obstacle.hp - 1 + + const currentFrame = frames.find( + (frame: { hp: number; index: number }) => obstacle.hp <= frame.hp + ) + + if (currentFrame) { + obstacle.animation.currentFrame = currentFrame.index + } + + if (obstacle.hp === 0) { + obstacles.splice(obstacles.indexOf(obstacle), 1) + } } diff --git a/packages/client/src/components/Game/player.tsx b/packages/client/src/components/Game/player.tsx index 92a46dc..c432ff7 100644 --- a/packages/client/src/components/Game/player.tsx +++ b/packages/client/src/components/Game/player.tsx @@ -6,7 +6,7 @@ export const PLAYER_DEFAULT_PARAMS = { y: 560, width: 70, height: 70, - speed: 2, + speed: 3, direction: { x: 0, y: 0 }, animation: { currentFrame: 0, // Текущий кадр спрайта diff --git a/packages/client/src/components/Game/utils.tsx b/packages/client/src/components/Game/utils.tsx index 64c25cb..13284bb 100644 --- a/packages/client/src/components/Game/utils.tsx +++ b/packages/client/src/components/Game/utils.tsx @@ -1,8 +1,18 @@ import enemiesSpritePath from '@/assets/images/sprites/enemy.png' import playerSpritePath from '@/assets/images/sprites/tank.png' import bulletSpritePath from '@/assets/images/sprites/bullet.png' -import wallSpritePath from '@/assets/images/sprites/wall.svg' -import { AbstractEntity, Enemy, Obstacle } from '@/components/Game/gameTypes' +import wallSpritePath from '@/assets/images/sprites/wall.png' +import steelSpritePath from '@/assets/images/sprites/steel.png' +import treeSpritePath from '@/assets/images/sprites/tree.png' +import bangSpritePath from '@/assets/images/sprites/bang.png' +import shotSpritePath from '@/assets/images/sprites/shot.png' +import { + AbstractEntity, + Effect, + Enemy, + Obstacle, +} from '@/components/Game/gameTypes' +import { deleteEffect } from './effects' export const getRandomEdgePosition = ( canvasWidth: number, @@ -53,15 +63,17 @@ const darawTank = ( } // Если смещение кратно 10 меняем кадр - if (moovment % animation.frameInterval === 0) { + if (animation?.frameInterval && moovment % animation.frameInterval === 0) { animation.currentFrame = (animation.currentFrame + 1) % animation?.totalFrames } - spriteSettings.width = sprite.width / animation.totalFrames - spriteSettings.height = sprite.height - spriteSettings.sourceX = animation.currentFrame * spriteSettings.width - spriteSettings.sourceY = 0 + if (animation) { + spriteSettings.width = sprite.width / animation.totalFrames + spriteSettings.height = sprite.height + spriteSettings.sourceX = animation.currentFrame * spriteSettings.width + spriteSettings.sourceY = 0 + } context.save() context.translate(data.x + data.width / 2, data.y + data.height / 2) @@ -96,8 +108,6 @@ const enemiesSprite = new Image() enemiesSprite.src = enemiesSpritePath -const lastEnemyDirection: Record = {} - export const drawEnemies = ( context: CanvasRenderingContext2D, enemies: Enemy[] @@ -108,29 +118,53 @@ export const drawEnemies = ( } const wallSprite = new Image() +const steelSprite = new Image() +const treeSprite = new Image() + wallSprite.src = wallSpritePath +steelSprite.src = steelSpritePath +treeSprite.src = treeSpritePath export const drawObstacles = ( context: CanvasRenderingContext2D, obstacles: Obstacle[] ) => { - const SPRITE_SIZE = 50 - obstacles.forEach(obstacle => { - const horizontalCount = Math.ceil(obstacle.width / SPRITE_SIZE) - const verticalCount = Math.ceil(obstacle.height / SPRITE_SIZE) + let sprite: HTMLImageElement + + switch (obstacle.type) { + case 'steel': + sprite = steelSprite + + break + case 'wall': + sprite = wallSprite + + break + case 'tree': + sprite = treeSprite + + break + + default: + sprite = treeSprite - Array.from({ length: horizontalCount }).forEach((_, i) => { - Array.from({ length: verticalCount }).forEach((_, j) => { - const x = obstacle.x + i * SPRITE_SIZE - const y = obstacle.y + j * SPRITE_SIZE + break + } - const width = Math.min(SPRITE_SIZE, obstacle.width - i * SPRITE_SIZE) - const height = Math.min(SPRITE_SIZE, obstacle.height - j * SPRITE_SIZE) + const spriteSize = sprite.width / obstacle.animation.totalFrames - context.drawImage(wallSprite, 0, 0, width, height, x, y, width, height) - }) - }) + context.drawImage( + sprite, + obstacle.animation.currentFrame * spriteSize, + 0, + spriteSize, + spriteSize, + obstacle.x, + obstacle.y, + spriteSize, + spriteSize + ) }) } @@ -162,3 +196,83 @@ export const drawBullets = ( context.restore() }) } + +const bangSprite = new Image() +const shotSprite = new Image() + +bangSprite.src = bangSpritePath +shotSprite.src = shotSpritePath + +export const drawEffects = ( + context: CanvasRenderingContext2D, + effects: Effect[] +) => { + effects.forEach(effect => { + const spriteSettings = { + width: 0, + height: 0, + sourceX: 0, + sourceY: 0, + } + let sprite: HTMLImageElement + + if (typeof effect.animation.frameCount === 'number') { + effect.animation.frameCount++ + + if ( + effect.animation?.frameInterval && + effect.animation.frameCount % effect.animation.frameInterval === 0 + ) { + effect.animation.currentFrame = effect.animation.currentFrame + 1 + + if ( + effect.animation.currentFrame === + effect.animation.totalFrames - 1 + ) { + deleteEffect(effect) + } + } + } + + switch (effect.type) { + case 'shot': + sprite = shotSprite + + break + + case 'bang': + sprite = bangSprite + + break + + default: + sprite = bangSprite + } + + spriteSettings.width = sprite.width / effect.animation.totalFrames + spriteSettings.height = sprite.height + spriteSettings.sourceX = + effect.animation.currentFrame * spriteSettings.width + spriteSettings.sourceY = 0 + + context.save() + context.translate(effect.x + effect.width / 2, effect.y + effect.height / 2) + + if (effect.direction) + context.rotate(Math.atan2(effect.direction.x, -effect.direction.y)) + + context.drawImage( + sprite, + spriteSettings.sourceX, + spriteSettings.sourceY, + spriteSettings.width, + spriteSettings.height, + -effect.width / 2, + -effect.height / 2, + effect.width, + effect.height + ) + + context.restore() + }) +}