From e781d4987a2d7a612f9d29692d626b0acdef4154 Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:45:44 +0300 Subject: [PATCH 01/13] =?UTF-8?q?[SOK-36]=20refactor:=20=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B5=D0=BD=D0=B5=D1=81=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B8=D0=B3=D1=80=D0=BE=D0=BA=D0=B0=20=D0=B2=20?= =?UTF-8?q?gameloop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/components/Game/Game.tsx | 19 +++----------- .../client/src/components/Game/gameLoop.tsx | 25 +++++++++++++++---- 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/packages/client/src/components/Game/Game.tsx b/packages/client/src/components/Game/Game.tsx index 46cc8c7..56b96df 100644 --- a/packages/client/src/components/Game/Game.tsx +++ b/packages/client/src/components/Game/Game.tsx @@ -3,12 +3,8 @@ import './Game.scss' import { initializeEnemies } from '@/components/Game/enemy' import { PLAYER_DEFAULT_PARAMS } from '@/components/Game/player' import { gameLoop } from '@/components/Game/gameLoop' -import { - handleKeyDown, - handleKeyUp, - updatePlayerMovement, -} from '@/components/Game/controls' -import { ControlsProps, Obstacle } from '@/components/Game/gameTypes' +import { handleKeyDown, handleKeyUp } from '@/components/Game/controls' +import { Obstacle } from '@/components/Game/gameTypes' import { initializeObstacle } from '@/components/Game/obstacle' import { Modal } from '../common/Modal/Modal' @@ -40,19 +36,12 @@ export const Game: React.FC = () => { if (!isPausedRef.current && !isGameOver && canvasRef.current) { const context = canvasRef.current.getContext('2d') if (context) { - const moveProps: ControlsProps = { - playerRef, - obstacles: obstaclesRef.current, - canvasWidth: canvasRef.current.width, - canvasHeight: canvasRef.current.height, - } - updatePlayerMovement(moveProps) - gameLoop( context, + canvasRef, playerRef, enemiesRef, - obstaclesRef.current, + obstaclesRef, livesRef, handleGameOver ) diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index d777848..5eb8d1b 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -1,23 +1,31 @@ import { HandlePlayerHit, resetPlayerPosition } from './player' import { updateEnemyPositions, respawnEnemies } from './enemy' import { clearCanvas, drawPlayer, drawEnemies, drawObstacles } from './utils' -import { Enemy, Obstacle, Player } from '@/components/Game/gameTypes' +import { + ControlsProps, + Enemy, + Obstacle, + Player, +} from '@/components/Game/gameTypes' import { detectEnemyCollision } from '@/components/Game/collision' +import { updatePlayerMovement } from '@/components/Game/controls' /** * Основной игровой цикл, который обновляет состояние игры и перерисовывает экран каждый кадр. * @param context - Контекст рисования для Canvas. + * @param canvasRef - Ссылка на Canvas. * @param playerRef - Ссылка на текущего игрока. * @param enemiesRef - Ссылка на массив врагов. - * @param obstacles - Массив препятствий. + * @param obstaclesRef - Ссылка на массив препятствий. * @param livesRef - Ссылка на текущее количество жизней игрока. * @param handleGameOver - Обработчик события окончания игры. */ export const gameLoop = ( context: CanvasRenderingContext2D, + canvasRef: React.MutableRefObject, playerRef: React.MutableRefObject, enemiesRef: React.MutableRefObject, - obstacles: Obstacle[], + obstaclesRef: React.MutableRefObject, livesRef: React.MutableRefObject, handleGameOver: () => void ) => { @@ -25,9 +33,16 @@ export const gameLoop = ( // Обновление позиций врагов updateEnemyPositions(playerRef.current, enemiesRef) - + if (!canvasRef.current) return + const moveProps: ControlsProps = { + playerRef, + obstacles: obstaclesRef.current, + canvasWidth: canvasRef.current.width, + canvasHeight: canvasRef.current.height, + } + updatePlayerMovement(moveProps) // Отрисовка всех игровых объектов - drawObstacles(context, obstacles) + drawObstacles(context, obstaclesRef.current) drawPlayer(context, playerRef.current) drawEnemies(context, enemiesRef.current) From 682d1c24fdacd907680116f1d3ec5a87f14495ce Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Fri, 11 Oct 2024 23:50:26 +0300 Subject: [PATCH 02/13] =?UTF-8?q?[SOK-36]=20refactor:=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=BE=D0=B1=D1=80=D0=B0=D0=B7=D0=BE=D0=B2=D0=B0=D0=BB=20=D0=B8?= =?UTF-8?q?=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D0=B2=D1=80?= =?UTF-8?q?=D0=B0=D0=B3=D0=B0=20=D0=B8=20=D0=B8=D0=B3=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=B2=20=D0=B0=D0=B1=D1=81=D1=82=D1=80=D0=B0=D0=BA?= =?UTF-8?q?=D1=82=D0=BD=D1=8B=D0=B5=20=D1=81=D1=83=D1=89=D0=BD=D0=BE=D1=81?= =?UTF-8?q?=D1=82=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/components/Game/collision.tsx | 8 ++++---- packages/client/src/components/Game/enemy.tsx | 16 +++++++++------- packages/client/src/components/Game/gameLoop.tsx | 7 +++---- .../client/src/components/Game/gameTypes.tsx | 13 ++----------- packages/client/src/components/Game/player.tsx | 4 ++-- packages/client/src/components/Game/utils.tsx | 6 +++--- 6 files changed, 23 insertions(+), 31 deletions(-) diff --git a/packages/client/src/components/Game/collision.tsx b/packages/client/src/components/Game/collision.tsx index 6eca1ae..287f436 100644 --- a/packages/client/src/components/Game/collision.tsx +++ b/packages/client/src/components/Game/collision.tsx @@ -1,7 +1,7 @@ -import { Enemy, Obstacle, Player } from '@/components/Game/gameTypes' +import { Obstacle, AbstractEntity } from '@/components/Game/gameTypes' export const detectCollision = ( - player: Player, + player: AbstractEntity, obstacle: Obstacle ): boolean => { return ( @@ -13,8 +13,8 @@ export const detectCollision = ( } export const detectEnemyCollision = ( - rect1: Player | Enemy, - rect2: Obstacle | Enemy + rect1: AbstractEntity, + rect2: Obstacle | AbstractEntity ): boolean => { return ( rect1.x < rect2.x + rect2.width && diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index 692b85f..f916a84 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -1,12 +1,12 @@ import { getRandomEdgePosition } from './utils' -import { Enemy, Player } from '@/components/Game/gameTypes' +import { AbstractEntity } from '@/components/Game/gameTypes' export const initializeEnemies = (numberOfEnemies: number) => { - const initialEnemies: Enemy[] = [] + const initialEnemies: AbstractEntity[] = [] for (let i = 0; i < numberOfEnemies; i++) { // количество врагов const { x, y } = getRandomEdgePosition(800, 600) - const enemy: Enemy = { + const enemy: AbstractEntity = { x, y, width: 30, @@ -16,12 +16,12 @@ export const initializeEnemies = (numberOfEnemies: number) => { } initialEnemies.push(enemy) } - return initialEnemies as Enemy[] + return initialEnemies as AbstractEntity[] } export const updateEnemyPositions = ( - player: Player, - enemiesRef: React.MutableRefObject + player: AbstractEntity, + enemiesRef: React.MutableRefObject ) => { enemiesRef.current = enemiesRef.current.map(enemy => { const directionX = player.x - enemy.x @@ -38,6 +38,8 @@ export const updateEnemyPositions = ( }) } -export const respawnEnemies = (enemiesRef: React.MutableRefObject) => { +export const respawnEnemies = ( + enemiesRef: React.MutableRefObject +) => { enemiesRef.current = initializeEnemies(5) } diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index 5eb8d1b..01e03ab 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -3,9 +3,8 @@ import { updateEnemyPositions, respawnEnemies } from './enemy' import { clearCanvas, drawPlayer, drawEnemies, drawObstacles } from './utils' import { ControlsProps, - Enemy, + AbstractEntity, Obstacle, - Player, } from '@/components/Game/gameTypes' import { detectEnemyCollision } from '@/components/Game/collision' import { updatePlayerMovement } from '@/components/Game/controls' @@ -23,8 +22,8 @@ import { updatePlayerMovement } from '@/components/Game/controls' export const gameLoop = ( context: CanvasRenderingContext2D, canvasRef: React.MutableRefObject, - playerRef: React.MutableRefObject, - enemiesRef: React.MutableRefObject, + playerRef: React.MutableRefObject, + enemiesRef: React.MutableRefObject, obstaclesRef: React.MutableRefObject, livesRef: React.MutableRefObject, handleGameOver: () => void diff --git a/packages/client/src/components/Game/gameTypes.tsx b/packages/client/src/components/Game/gameTypes.tsx index c59e901..b2c0b86 100644 --- a/packages/client/src/components/Game/gameTypes.tsx +++ b/packages/client/src/components/Game/gameTypes.tsx @@ -1,13 +1,4 @@ -export interface Player { - x: number - y: number - width: number - height: number - speed: number - direction: { x: number; y: number } -} - -export interface Enemy { +export interface AbstractEntity { x: number y: number width: number @@ -24,7 +15,7 @@ export interface Obstacle { } export interface ControlsProps { - playerRef: React.MutableRefObject + playerRef: React.MutableRefObject obstacles: Obstacle[] canvasWidth: number canvasHeight: number diff --git a/packages/client/src/components/Game/player.tsx b/packages/client/src/components/Game/player.tsx index 4ffca5a..614f983 100644 --- a/packages/client/src/components/Game/player.tsx +++ b/packages/client/src/components/Game/player.tsx @@ -1,4 +1,4 @@ -import { Player } from '@/components/Game/gameTypes' +import { AbstractEntity } from '@/components/Game/gameTypes' export const PLAYER_DEFAULT_PARAMS = { x: 400, @@ -10,7 +10,7 @@ export const PLAYER_DEFAULT_PARAMS = { } export const resetPlayerPosition = ( - playerRef: React.MutableRefObject + playerRef: React.MutableRefObject ) => { playerRef.current = { ...playerRef.current, diff --git a/packages/client/src/components/Game/utils.tsx b/packages/client/src/components/Game/utils.tsx index 59bec75..47d680f 100644 --- a/packages/client/src/components/Game/utils.tsx +++ b/packages/client/src/components/Game/utils.tsx @@ -1,7 +1,7 @@ import enemiesSpritePath from '@/assets/images/sprites/enemy.svg' import tankSpritePath from '@/assets/images/sprites/tank.svg' import wallSpritePath from '@/assets/images/sprites/wall.svg' -import { Enemy, Obstacle, Player } from '@/components/Game/gameTypes' +import { AbstractEntity, Obstacle } from '@/components/Game/gameTypes' export const getRandomEdgePosition = ( canvasWidth: number, @@ -32,7 +32,7 @@ let lastPlayerDirection = { x: 0, y: 0 } export const drawPlayer = ( context: CanvasRenderingContext2D, - player: Player + player: AbstractEntity ) => { let direction = { ...player.direction } @@ -67,7 +67,7 @@ enemiesSprite.src = enemiesSpritePath export const drawEnemies = ( context: CanvasRenderingContext2D, - enemies: Enemy[] + enemies: AbstractEntity[] ) => { enemies.forEach(enemy => { context.drawImage(enemiesSprite, enemy.x, enemy.y) From 5e2f486a2abcadf249bb2d140e91f45e0eb8dcac Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Sat, 12 Oct 2024 00:34:18 +0300 Subject: [PATCH 03/13] =?UTF-8?q?[SOK-36]=20feat:=20=D1=81=D0=BE=D0=B7?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=20=D0=BF=D1=83=D0=BB=D0=B8,=20=D0=BD=D0=B0?= =?UTF-8?q?=D1=81=D1=82=D1=80=D0=BE=D0=B8=D0=BB=20=D0=B2=D1=8B=D1=81=D1=82?= =?UTF-8?q?=D1=80=D0=B5=D0=BB=D1=8B=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE?= =?UTF-8?q?=D0=B2=D0=B0=D1=82=D0=B5=D0=BB=D0=B5=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/components/Game/Game.tsx | 4 +- .../client/src/components/Game/bullet.tsx | 31 +++++++++++++ .../client/src/components/Game/collision.tsx | 12 +++++ .../client/src/components/Game/controls.tsx | 30 ++++++++----- packages/client/src/components/Game/enemy.tsx | 7 +++ .../client/src/components/Game/gameLoop.tsx | 44 ++++++++++++++++--- .../client/src/components/Game/gameTypes.tsx | 1 + packages/client/src/components/Game/utils.tsx | 10 +++++ 8 files changed, 121 insertions(+), 18 deletions(-) create mode 100644 packages/client/src/components/Game/bullet.tsx diff --git a/packages/client/src/components/Game/Game.tsx b/packages/client/src/components/Game/Game.tsx index 56b96df..6adfdcf 100644 --- a/packages/client/src/components/Game/Game.tsx +++ b/packages/client/src/components/Game/Game.tsx @@ -4,7 +4,7 @@ import { initializeEnemies } from '@/components/Game/enemy' import { PLAYER_DEFAULT_PARAMS } from '@/components/Game/player' import { gameLoop } from '@/components/Game/gameLoop' import { handleKeyDown, handleKeyUp } from '@/components/Game/controls' -import { Obstacle } from '@/components/Game/gameTypes' +import { AbstractEntity, Obstacle } from '@/components/Game/gameTypes' import { initializeObstacle } from '@/components/Game/obstacle' import { Modal } from '../common/Modal/Modal' @@ -14,6 +14,7 @@ export const Game: React.FC = () => { const canvasRef = useRef(null) const playerRef = useRef(PLAYER_DEFAULT_PARAMS) const enemiesRef = useRef(initializeEnemies(5)) + const bulletsRef = useRef([]) const obstaclesRef = useRef(initializeObstacle()) const livesRef = useRef(livesUse) const [gameStarted, setGameStarted] = useState(false) @@ -41,6 +42,7 @@ export const Game: React.FC = () => { canvasRef, playerRef, enemiesRef, + bulletsRef, obstaclesRef, livesRef, handleGameOver diff --git a/packages/client/src/components/Game/bullet.tsx b/packages/client/src/components/Game/bullet.tsx new file mode 100644 index 0000000..b4b5ff3 --- /dev/null +++ b/packages/client/src/components/Game/bullet.tsx @@ -0,0 +1,31 @@ +import { AbstractEntity } from '@/components/Game/gameTypes' + +export const createBullet = (player: AbstractEntity): AbstractEntity => { + return { + x: player.x + player.width / 2 - 2.5, // Пуля появляется в центре танка + y: player.y + player.height / 2 - 2.5, + width: 5, + height: 5, + speed: 5, // Скорость пули + direction: player.direction, // Пуля летит в направлении танка + } +} + +export const updateBullets = ( + bullets: AbstractEntity[], + canvasWidth: number, + canvasHeight: number +) => { + return bullets.filter(bullet => { + bullet.x += bullet.direction.x * bullet.speed + bullet.y += bullet.direction.y * bullet.speed + + // Удаляем пулю, если она выходит за пределы экрана + return ( + bullet.x >= 0 && + bullet.x <= canvasWidth && + bullet.y >= 0 && + bullet.y <= canvasHeight + ) + }) +} diff --git a/packages/client/src/components/Game/collision.tsx b/packages/client/src/components/Game/collision.tsx index 287f436..aba48a6 100644 --- a/packages/client/src/components/Game/collision.tsx +++ b/packages/client/src/components/Game/collision.tsx @@ -23,3 +23,15 @@ export const detectEnemyCollision = ( rect1.y + rect1.height > rect2.y ) } + +export const detectBulletCollision = ( + bullet: AbstractEntity, + entity: AbstractEntity +): boolean => { + return ( + bullet.x < entity.x + entity.width && + bullet.x + bullet.width > entity.x && + bullet.y < entity.y + entity.height && + bullet.y + bullet.height > entity.y + ) +} diff --git a/packages/client/src/components/Game/controls.tsx b/packages/client/src/components/Game/controls.tsx index 9e71fc7..9e76a76 100644 --- a/packages/client/src/components/Game/controls.tsx +++ b/packages/client/src/components/Game/controls.tsx @@ -1,6 +1,7 @@ import { ControlsProps } from '@/components/Game/gameTypes' import { detectCollision } from '@/components/Game/collision' +import { createBullet } from '@/components/Game/bullet' let pressedKeys: string[] = [] @@ -9,6 +10,7 @@ enum Action { MoveDown = 'MoveDown', MoveLeft = 'MoveLeft', MoveRight = 'MoveRight', + Shoot = 'Shoot', } type Vector = { @@ -16,11 +18,12 @@ type Vector = { y: -1 | 0 | 1 } -const MOVEMENT_CONTROLS: Record = { +const ACTION_CONTROLS: Record = { [Action.MoveUp]: ['ArrowUp', 'w', 'ц', 'W', 'Ц'], [Action.MoveDown]: ['ArrowDown', 's', 'ы', 'S', 'Ы'], [Action.MoveLeft]: ['ArrowLeft', 'a', 'ф', 'A', 'Ф'], [Action.MoveRight]: ['ArrowRight', 'd', 'в', 'D', 'В'], + [Action.Shoot]: [' '], } const VECTORS: Record = { @@ -28,6 +31,7 @@ const VECTORS: Record = { [Action.MoveDown]: { x: 0, y: 1 }, [Action.MoveLeft]: { x: -1, y: 0 }, [Action.MoveRight]: { x: 1, y: 0 }, + [Action.Shoot]: { x: 0, y: 0 }, } export const handleKeyDown = (key: string) => { @@ -40,8 +44,8 @@ export const handleKeyUp = (key: string) => { pressedKeys = pressedKeys.filter(currentKey => currentKey !== key) } -const getMovementControlByKey = (key: string): Action | null => { - for (const [action, keys] of Object.entries(MOVEMENT_CONTROLS)) { +const getActionControlByKey = (key: string): Action | null => { + for (const [action, keys] of Object.entries(ACTION_CONTROLS)) { if (keys.includes(key)) { return action as Action } @@ -50,9 +54,9 @@ const getMovementControlByKey = (key: string): Action | null => { return null } -const getLastMovementAction = (): Action | null => { +const getLastAction = (): Action | null => { for (let i = pressedKeys.length - 1; i >= 0; i--) { - const action = getMovementControlByKey(pressedKeys[i]) + const action = getActionControlByKey(pressedKeys[i]) if (action) { return action @@ -62,24 +66,26 @@ const getLastMovementAction = (): Action | null => { return null } -export const updatePlayerMovement = (props: ControlsProps) => { +export const updatePlayerAction = (props: ControlsProps) => { const speed = props.playerRef.current.speed let newX = props.playerRef.current.x let newY = props.playerRef.current.y - const lastMovementAction = getLastMovementAction() + const lastMovementAction = getLastAction() if (!lastMovementAction) return const vector = VECTORS[lastMovementAction] if (!vector) return + if (lastMovementAction === Action.Shoot) { + props.bulletsRef.current.push(createBullet(props.playerRef.current)) + } else { + props.playerRef.current.direction = vector - props.playerRef.current.direction = vector - - newX += vector.x * speed - newY += vector.y * speed - + newX += vector.x * speed + newY += vector.y * speed + } const hasCollision = props.obstacles.some(obstacle => { return detectCollision( { ...props.playerRef.current, x: newX, y: newY }, diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index f916a84..b1a33b0 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -43,3 +43,10 @@ export const respawnEnemies = ( ) => { enemiesRef.current = initializeEnemies(5) } + +export const killEnemy = ( + enemiesRef: React.MutableRefObject, + enemy: AbstractEntity +) => { + enemiesRef.current = enemiesRef.current.filter(e => e !== enemy) +} diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index 01e03ab..73fd08e 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -1,13 +1,23 @@ import { HandlePlayerHit, resetPlayerPosition } from './player' -import { updateEnemyPositions, respawnEnemies } from './enemy' -import { clearCanvas, drawPlayer, drawEnemies, drawObstacles } from './utils' +import { updateEnemyPositions, respawnEnemies, killEnemy } from './enemy' +import { + clearCanvas, + drawPlayer, + drawEnemies, + drawObstacles, + drawBullets, +} from './utils' import { ControlsProps, AbstractEntity, Obstacle, } from '@/components/Game/gameTypes' -import { detectEnemyCollision } from '@/components/Game/collision' -import { updatePlayerMovement } from '@/components/Game/controls' +import { + detectBulletCollision, + detectEnemyCollision, +} from '@/components/Game/collision' +import { updatePlayerAction } from '@/components/Game/controls' +import { updateBullets } from '@/components/Game/bullet' /** * Основной игровой цикл, который обновляет состояние игры и перерисовывает экран каждый кадр. @@ -15,6 +25,7 @@ import { updatePlayerMovement } from '@/components/Game/controls' * @param canvasRef - Ссылка на Canvas. * @param playerRef - Ссылка на текущего игрока. * @param enemiesRef - Ссылка на массив врагов. + * @param bulletsRef - Ссылка на массив пуль. * @param obstaclesRef - Ссылка на массив препятствий. * @param livesRef - Ссылка на текущее количество жизней игрока. * @param handleGameOver - Обработчик события окончания игры. @@ -24,6 +35,7 @@ export const gameLoop = ( canvasRef: React.MutableRefObject, playerRef: React.MutableRefObject, enemiesRef: React.MutableRefObject, + bulletsRef: React.MutableRefObject, obstaclesRef: React.MutableRefObject, livesRef: React.MutableRefObject, handleGameOver: () => void @@ -35,15 +47,37 @@ export const gameLoop = ( if (!canvasRef.current) return const moveProps: ControlsProps = { playerRef, + bulletsRef, obstacles: obstaclesRef.current, canvasWidth: canvasRef.current.width, canvasHeight: canvasRef.current.height, } - updatePlayerMovement(moveProps) + updatePlayerAction(moveProps) + + bulletsRef.current = updateBullets( + bulletsRef.current, + canvasRef.current.width, + canvasRef.current.height + ) + // Отрисовка всех игровых объектов drawObstacles(context, obstaclesRef.current) drawPlayer(context, playerRef.current) drawEnemies(context, enemiesRef.current) + drawBullets(context, bulletsRef.current) // Отрисовка пуль + + // Проверка на столкновения пуль с врагами + bulletsRef.current.forEach(bullet => { + enemiesRef.current = enemiesRef.current.filter(enemy => { + const hit = detectBulletCollision(bullet, enemy) + if (hit) { + // Убираем врага, если попали + killEnemy(enemiesRef, enemy) + return false + } + return true + }) + }) // Проверка на столкновения между игроком и врагами enemiesRef.current.forEach(enemy => { diff --git a/packages/client/src/components/Game/gameTypes.tsx b/packages/client/src/components/Game/gameTypes.tsx index b2c0b86..00d0902 100644 --- a/packages/client/src/components/Game/gameTypes.tsx +++ b/packages/client/src/components/Game/gameTypes.tsx @@ -16,6 +16,7 @@ export interface Obstacle { export interface ControlsProps { playerRef: React.MutableRefObject + bulletsRef: React.MutableRefObject obstacles: Obstacle[] canvasWidth: number canvasHeight: number diff --git a/packages/client/src/components/Game/utils.tsx b/packages/client/src/components/Game/utils.tsx index 47d680f..388131c 100644 --- a/packages/client/src/components/Game/utils.tsx +++ b/packages/client/src/components/Game/utils.tsx @@ -85,3 +85,13 @@ export const drawObstacles = ( context.drawImage(wallSprite, obstacle.x, obstacle.y) }) } + +export const drawBullets = ( + context: CanvasRenderingContext2D, + bullets: AbstractEntity[] +) => { + context.fillStyle = 'yellow' + bullets.forEach(bullet => { + context.fillRect(bullet.x, bullet.y, bullet.width, bullet.height) + }) +} From 4ccd71696474d0657cd2d8ad9161cc880d841abd Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Sat, 12 Oct 2024 00:55:31 +0300 Subject: [PATCH 04/13] =?UTF-8?q?[SOK-36]=20feat:=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B8=D0=BB=20=D0=BF=D0=BE=D0=BF=D0=B0=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=83=D0=BB=D0=B5=D0=B9=20?= =?UTF-8?q?=D0=B8=20=D0=B2=D1=8B=D0=BF=D1=83=D1=81=D0=BA=20=D0=BF=D0=BE=20?= =?UTF-8?q?=D0=BE=D0=B4=D0=BD=D0=BE=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/components/Game/bullet.tsx | 4 ++-- .../client/src/components/Game/controls.tsx | 19 ++++++++++++++++++- packages/client/src/components/Game/enemy.tsx | 4 ++-- .../client/src/components/Game/gameLoop.tsx | 2 ++ 4 files changed, 24 insertions(+), 5 deletions(-) diff --git a/packages/client/src/components/Game/bullet.tsx b/packages/client/src/components/Game/bullet.tsx index b4b5ff3..6ac1fcf 100644 --- a/packages/client/src/components/Game/bullet.tsx +++ b/packages/client/src/components/Game/bullet.tsx @@ -4,8 +4,8 @@ export const createBullet = (player: AbstractEntity): AbstractEntity => { return { x: player.x + player.width / 2 - 2.5, // Пуля появляется в центре танка y: player.y + player.height / 2 - 2.5, - width: 5, - height: 5, + width: 10, + height: 10, speed: 5, // Скорость пули direction: player.direction, // Пуля летит в направлении танка } diff --git a/packages/client/src/components/Game/controls.tsx b/packages/client/src/components/Game/controls.tsx index 9e76a76..2c8673e 100644 --- a/packages/client/src/components/Game/controls.tsx +++ b/packages/client/src/components/Game/controls.tsx @@ -4,6 +4,7 @@ import { detectCollision } from '@/components/Game/collision' import { createBullet } from '@/components/Game/bullet' let pressedKeys: string[] = [] +let shootPressed = false // Флаг для стрельбы enum Action { MoveUp = 'MoveUp', @@ -38,10 +39,20 @@ export const handleKeyDown = (key: string) => { if (!pressedKeys.includes(key)) { pressedKeys.push(key) } + + // Устанавливаем флаг, если нажата клавиша пробела (стрельба) + if (key === ' ') { + shootPressed = true + } } export const handleKeyUp = (key: string) => { pressedKeys = pressedKeys.filter(currentKey => currentKey !== key) + + // Сбрасываем флаг, если клавиша пробела отпущена + if (key === ' ') { + shootPressed = false + } } const getActionControlByKey = (key: string): Action | null => { @@ -78,14 +89,20 @@ export const updatePlayerAction = (props: ControlsProps) => { const vector = VECTORS[lastMovementAction] if (!vector) return + if (lastMovementAction === Action.Shoot) { - props.bulletsRef.current.push(createBullet(props.playerRef.current)) + // Стреляем только при первом нажатии на пробел + if (shootPressed) { + props.bulletsRef.current.push(createBullet(props.playerRef.current)) + shootPressed = false // Сбрасываем флаг после выстрела + } } else { props.playerRef.current.direction = vector newX += vector.x * speed newY += vector.y * speed } + const hasCollision = props.obstacles.some(obstacle => { return detectCollision( { ...props.playerRef.current, x: newX, y: newY }, diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index b1a33b0..7276c8f 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -9,8 +9,8 @@ export const initializeEnemies = (numberOfEnemies: number) => { const enemy: AbstractEntity = { x, y, - width: 30, - height: 30, + width: 70, + height: 70, speed: 1, direction: { x: 0, y: 0 }, } diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index 73fd08e..333a391 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -73,6 +73,8 @@ export const gameLoop = ( if (hit) { // Убираем врага, если попали killEnemy(enemiesRef, enemy) + // Убираем пулю, если попали + bulletsRef.current = bulletsRef.current.filter(b => b !== bullet) return false } return true From 28eaadf332545e878edc45b54d61409375026c07 Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Sat, 12 Oct 2024 01:01:54 +0300 Subject: [PATCH 05/13] =?UTF-8?q?[SOK-36]=20fix:=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B8=D0=BB=20=D0=BE=D0=B3=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=87=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=20=D1=81?= =?UTF-8?q?=D0=BA=D0=BE=D1=80=D0=BE=D1=81=D1=82=D0=B8=20=D0=B2=D1=8B=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=B5=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/components/Game/controls.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/client/src/components/Game/controls.tsx b/packages/client/src/components/Game/controls.tsx index 2c8673e..1a1945f 100644 --- a/packages/client/src/components/Game/controls.tsx +++ b/packages/client/src/components/Game/controls.tsx @@ -1,10 +1,11 @@ import { ControlsProps } from '@/components/Game/gameTypes' - import { detectCollision } from '@/components/Game/collision' import { createBullet } from '@/components/Game/bullet' let pressedKeys: string[] = [] let shootPressed = false // Флаг для стрельбы +let lastShotTime = 0 // Время последнего выстрела +const SHOOT_DELAY = 500 // Задержка между выстрелами (в миллисекундах) enum Action { MoveUp = 'MoveUp', @@ -90,11 +91,13 @@ export const updatePlayerAction = (props: ControlsProps) => { if (!vector) return + const currentTime = Date.now() // Получаем текущее время + if (lastMovementAction === Action.Shoot) { - // Стреляем только при первом нажатии на пробел - if (shootPressed) { + // Ограничение скорости стрельбы с помощью таймера + if (shootPressed && currentTime - lastShotTime >= SHOOT_DELAY) { props.bulletsRef.current.push(createBullet(props.playerRef.current)) - shootPressed = false // Сбрасываем флаг после выстрела + lastShotTime = currentTime // Обновляем время последнего выстрела } } else { props.playerRef.current.direction = vector From ebe6c70e14f1231f636c58c4fd1dd5a7260fbf4f Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Sat, 12 Oct 2024 01:24:19 +0300 Subject: [PATCH 06/13] =?UTF-8?q?[SOK-36]=20feat:=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B8=D0=BB=20=D0=B4=D0=B2=D0=B8=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B2=D1=80=D0=B0=D0=B3=D0=BE=D0=B2=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BD=D1=91=D0=BC,=20=D0=B0=20=D1=82=D0=B0=D0=BA?= =?UTF-8?q?=D0=B6=D0=B5=20=D0=BF=D0=BE=D0=B2=D0=BE=D1=80=D0=BE=D1=82=20?= =?UTF-8?q?=D1=81=D0=BF=D1=80=D0=B0=D0=B9=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/client/src/components/Game/enemy.tsx | 59 ++++++++++++++----- .../client/src/components/Game/gameLoop.tsx | 3 +- .../client/src/components/Game/gameTypes.tsx | 4 ++ packages/client/src/components/Game/utils.tsx | 36 ++++++++++- 4 files changed, 83 insertions(+), 19 deletions(-) diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index 7276c8f..c84084d 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -1,12 +1,13 @@ import { getRandomEdgePosition } from './utils' -import { AbstractEntity } from '@/components/Game/gameTypes' +import { AbstractEntity, Enemy } from '@/components/Game/gameTypes' export const initializeEnemies = (numberOfEnemies: number) => { - const initialEnemies: AbstractEntity[] = [] + const initialEnemies: Enemy[] = [] for (let i = 0; i < numberOfEnemies; i++) { // количество врагов const { x, y } = getRandomEdgePosition(800, 600) - const enemy: AbstractEntity = { + const enemy: Enemy = { + id: i, x, y, width: 70, @@ -16,36 +17,64 @@ export const initializeEnemies = (numberOfEnemies: number) => { } initialEnemies.push(enemy) } - return initialEnemies as AbstractEntity[] + return initialEnemies as Enemy[] } export const updateEnemyPositions = ( player: AbstractEntity, - enemiesRef: React.MutableRefObject + enemiesRef: React.MutableRefObject ) => { enemiesRef.current = enemiesRef.current.map(enemy => { + // Определяем разницу по X и Y const directionX = player.x - enemy.x const directionY = player.y - enemy.y - const magnitude = Math.sqrt(directionX ** 2 + directionY ** 2) - const normalizedX = directionX / magnitude - const normalizedY = directionY / magnitude + // Выбираем ближайшую ось для начала движения + const moveAlongX = Math.abs(directionX) > Math.abs(directionY) - const newX = enemy.x + normalizedX * enemy.speed - const newY = enemy.y + normalizedY * enemy.speed + let stepX = 0 + let stepY = 0 - return { ...enemy, x: newX, y: newY } + let newDirection: { x: number; y: number } + + if (moveAlongX) { + // Если разница по X больше, двигаемся по X + if (Math.abs(directionX) > enemy.speed) { + stepX = Math.sign(directionX) * enemy.speed + newDirection = { x: Math.sign(directionX), y: 0 } // направление по X + } else { + // Если по оси X выровнялись, двигаемся по Y + stepY = Math.sign(directionY) * enemy.speed + newDirection = { x: 0, y: Math.sign(directionY) } // направление по Y + } + } else { + // Если разница по Y больше, двигаемся по Y + if (Math.abs(directionY) > enemy.speed) { + stepY = Math.sign(directionY) * enemy.speed + newDirection = { x: 0, y: Math.sign(directionY) } // направление по Y + } else { + // Если по оси Y выровнялись, двигаемся по X + stepX = Math.sign(directionX) * enemy.speed + newDirection = { x: Math.sign(directionX), y: 0 } // направление по X + } + } + + const newX = enemy.x + stepX + const newY = enemy.y + stepY + + // Обновляем направление врага + enemy.direction = newDirection + + return { ...enemy, x: newX, y: newY, direction: newDirection } }) } -export const respawnEnemies = ( - enemiesRef: React.MutableRefObject -) => { +export const respawnEnemies = (enemiesRef: React.MutableRefObject) => { enemiesRef.current = initializeEnemies(5) } export const killEnemy = ( - enemiesRef: React.MutableRefObject, + enemiesRef: React.MutableRefObject, enemy: AbstractEntity ) => { enemiesRef.current = enemiesRef.current.filter(e => e !== enemy) diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index 333a391..154fbcb 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -11,6 +11,7 @@ import { ControlsProps, AbstractEntity, Obstacle, + Enemy, } from '@/components/Game/gameTypes' import { detectBulletCollision, @@ -34,7 +35,7 @@ export const gameLoop = ( context: CanvasRenderingContext2D, canvasRef: React.MutableRefObject, playerRef: React.MutableRefObject, - enemiesRef: React.MutableRefObject, + enemiesRef: React.MutableRefObject, bulletsRef: React.MutableRefObject, obstaclesRef: React.MutableRefObject, livesRef: React.MutableRefObject, diff --git a/packages/client/src/components/Game/gameTypes.tsx b/packages/client/src/components/Game/gameTypes.tsx index 00d0902..1391a3a 100644 --- a/packages/client/src/components/Game/gameTypes.tsx +++ b/packages/client/src/components/Game/gameTypes.tsx @@ -7,6 +7,10 @@ export interface AbstractEntity { direction: { x: number; y: number } } +export interface Enemy extends AbstractEntity { + id: number +} + export interface Obstacle { x: number y: number diff --git a/packages/client/src/components/Game/utils.tsx b/packages/client/src/components/Game/utils.tsx index 388131c..e8ef626 100644 --- a/packages/client/src/components/Game/utils.tsx +++ b/packages/client/src/components/Game/utils.tsx @@ -1,7 +1,7 @@ import enemiesSpritePath from '@/assets/images/sprites/enemy.svg' import tankSpritePath from '@/assets/images/sprites/tank.svg' import wallSpritePath from '@/assets/images/sprites/wall.svg' -import { AbstractEntity, Obstacle } from '@/components/Game/gameTypes' +import { AbstractEntity, Enemy, Obstacle } from '@/components/Game/gameTypes' export const getRandomEdgePosition = ( canvasWidth: number, @@ -65,12 +65,42 @@ export const drawPlayer = ( const enemiesSprite = new Image() enemiesSprite.src = enemiesSpritePath +const lastEnemyDirection: Record = {} + export const drawEnemies = ( context: CanvasRenderingContext2D, - enemies: AbstractEntity[] + enemies: Enemy[] ) => { enemies.forEach(enemy => { - context.drawImage(enemiesSprite, enemy.x, enemy.y) + let direction = { ...enemy.direction } + + // Если враг не двигается, используем последнее направление + const isEnemyIdle = direction.x === 0 && direction.y === 0 + if (isEnemyIdle) { + direction = lastEnemyDirection[enemy.id] || { x: 1, y: 0 } // по умолчанию вправо + } else { + lastEnemyDirection[enemy.id] = direction // сохраняем последнее направление для врага + } + + context.save() + + // Перемещаем контекст на позицию врага + context.translate(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2) + + // Вычисляем угол поворота на основе направления + const angle = Math.atan2(direction.x, -direction.y) + context.rotate(angle) + + // Отрисовываем спрайт врага с учётом его ширины и высоты + context.drawImage( + enemiesSprite, + -enemy.width / 2, + -enemy.height / 2, + enemy.width, + enemy.height + ) + + context.restore() }) } From c6ab430618a22f7944bdd43950959d0290adcf01 Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Sat, 12 Oct 2024 01:41:46 +0300 Subject: [PATCH 07/13] =?UTF-8?q?[SOK-36]=20feat:=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B8=D0=BB=20=D0=B2=D1=8B=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B5=D0=BB=D1=8B=20=D0=B2=D1=80=D0=B0=D0=B3=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/components/Game/bullet.tsx | 43 +++++++++++++++---- packages/client/src/components/Game/enemy.tsx | 18 ++++++++ .../client/src/components/Game/gameLoop.tsx | 20 ++++++++- .../client/src/components/Game/gameTypes.tsx | 1 + 4 files changed, 73 insertions(+), 9 deletions(-) diff --git a/packages/client/src/components/Game/bullet.tsx b/packages/client/src/components/Game/bullet.tsx index 6ac1fcf..cae0622 100644 --- a/packages/client/src/components/Game/bullet.tsx +++ b/packages/client/src/components/Game/bullet.tsx @@ -1,13 +1,40 @@ -import { AbstractEntity } from '@/components/Game/gameTypes' +import { AbstractEntity, Enemy } from '@/components/Game/gameTypes' + +export const createBullet = (enemy: Enemy): AbstractEntity => { + const bulletSpeed = 5 // Задайте скорость пули + const bulletDirection = { x: enemy.direction.x, y: enemy.direction.y } + + // Начальная позиция пули в зависимости от направления врага + let bulletX, bulletY + + if (enemy.direction.y < 0) { + // Вверх + bulletX = enemy.x + enemy.width / 2 // Центр по X + bulletY = enemy.y // Верхняя часть врага + } else if (enemy.direction.y > 0) { + // Вниз + bulletX = enemy.x + enemy.width / 2 // Центр по X + bulletY = enemy.y + enemy.height // Нижняя часть врага + } else if (enemy.direction.x < 0) { + // Влево + bulletX = enemy.x // Левый край врага + bulletY = enemy.y + enemy.height / 2 // Центр по Y + } else if (enemy.direction.x > 0) { + // Вправо + bulletX = enemy.x + enemy.width // Правый край врага + bulletY = enemy.y + enemy.height / 2 // Центр по Y + } else { + bulletX = enemy.x + enemy.width / 2 // По умолчанию - центр + bulletY = enemy.y + enemy.height / 2 // По умолчанию - центр + } -export const createBullet = (player: AbstractEntity): AbstractEntity => { return { - x: player.x + player.width / 2 - 2.5, // Пуля появляется в центре танка - y: player.y + player.height / 2 - 2.5, - width: 10, - height: 10, - speed: 5, // Скорость пули - direction: player.direction, // Пуля летит в направлении танка + x: bulletX, + y: bulletY, + width: 5, // Ширина пули + height: 5, // Высота пули + speed: bulletSpeed, + direction: bulletDirection, } } diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index c84084d..769ea67 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -1,5 +1,6 @@ import { getRandomEdgePosition } from './utils' import { AbstractEntity, Enemy } from '@/components/Game/gameTypes' +import { createBullet } from '@/components/Game/bullet' export const initializeEnemies = (numberOfEnemies: number) => { const initialEnemies: Enemy[] = [] @@ -79,3 +80,20 @@ export const killEnemy = ( ) => { enemiesRef.current = enemiesRef.current.filter(e => e !== enemy) } + +export const handleEnemyShooting = ( + enemies: Enemy[], + bulletsRef: React.MutableRefObject +) => { + const currentTime = Date.now() // Текущее время в миллисекундах + + enemies.forEach(enemy => { + // Проверяем, если прошло больше 2 секунд (2000 миллисекунд) с последнего выстрела + if (!enemy.lastShotTime || currentTime - enemy.lastShotTime >= 2000) { + bulletsRef.current.push(createBullet(enemy)) // Создаём новую пулю для врага + + // Обновляем время последнего выстрела + enemy.lastShotTime = currentTime + } + }) +} diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index 154fbcb..a2ba21a 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -1,5 +1,10 @@ import { HandlePlayerHit, resetPlayerPosition } from './player' -import { updateEnemyPositions, respawnEnemies, killEnemy } from './enemy' +import { + updateEnemyPositions, + respawnEnemies, + killEnemy, + handleEnemyShooting, +} from './enemy' import { clearCanvas, drawPlayer, @@ -55,6 +60,9 @@ export const gameLoop = ( } updatePlayerAction(moveProps) + // Стрельба врагов каждые 2 секунды + handleEnemyShooting(enemiesRef.current, bulletsRef) + bulletsRef.current = updateBullets( bulletsRef.current, canvasRef.current.width, @@ -80,6 +88,16 @@ export const gameLoop = ( } return true }) + if (detectBulletCollision(bullet, playerRef.current)) { + // Уменьшаем жизни игрока + livesRef.current -= 1 + // Удаляем пулю после попадания + bulletsRef.current = bulletsRef.current.filter(b => b !== bullet) + // Проверка на окончание игры + if (livesRef.current <= 0) { + handleGameOver() + } + } }) // Проверка на столкновения между игроком и врагами diff --git a/packages/client/src/components/Game/gameTypes.tsx b/packages/client/src/components/Game/gameTypes.tsx index 1391a3a..961d701 100644 --- a/packages/client/src/components/Game/gameTypes.tsx +++ b/packages/client/src/components/Game/gameTypes.tsx @@ -9,6 +9,7 @@ export interface AbstractEntity { export interface Enemy extends AbstractEntity { id: number + lastShotTime?: number } export interface Obstacle { From 9727692239704290aa6801bcce6ceb1d5d161437 Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Sat, 12 Oct 2024 01:49:58 +0300 Subject: [PATCH 08/13] =?UTF-8?q?[SOK-36]=20fix:=20=D0=B8=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D1=80=D0=B5=D0=BF=D0=B0=D1=83?= =?UTF-8?q?=D0=BD=20=D0=B8=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B8=D0=B7=D0=B8?= =?UTF-8?q?=D1=8E=20=D0=B2=D1=80=D0=B0=D0=B3=D0=BE=D0=B2=20=D0=B4=D1=80?= =?UTF-8?q?=D1=83=D0=B3=20=D1=81=20=D0=B4=D1=80=D1=83=D0=B3=D0=BE=D0=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/components/Game/bullet.tsx | 4 +- packages/client/src/components/Game/enemy.tsx | 58 +++++++++++++++++-- .../client/src/components/Game/gameLoop.tsx | 2 +- 3 files changed, 56 insertions(+), 8 deletions(-) diff --git a/packages/client/src/components/Game/bullet.tsx b/packages/client/src/components/Game/bullet.tsx index cae0622..2a69dc4 100644 --- a/packages/client/src/components/Game/bullet.tsx +++ b/packages/client/src/components/Game/bullet.tsx @@ -1,6 +1,6 @@ -import { AbstractEntity, Enemy } from '@/components/Game/gameTypes' +import { AbstractEntity } from '@/components/Game/gameTypes' -export const createBullet = (enemy: Enemy): AbstractEntity => { +export const createBullet = (enemy: AbstractEntity): AbstractEntity => { const bulletSpeed = 5 // Задайте скорость пули const bulletDirection = { x: enemy.direction.x, y: enemy.direction.y } diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index 769ea67..7037715 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -1,6 +1,7 @@ import { getRandomEdgePosition } from './utils' import { AbstractEntity, Enemy } from '@/components/Game/gameTypes' import { createBullet } from '@/components/Game/bullet' +import { detectCollision } from '@/components/Game/collision' export const initializeEnemies = (numberOfEnemies: number) => { const initialEnemies: Enemy[] = [] @@ -63,15 +64,62 @@ export const updateEnemyPositions = ( const newX = enemy.x + stepX const newY = enemy.y + stepY - // Обновляем направление врага - enemy.direction = newDirection + // Проверка столкновений с другими врагами + const hasCollision = enemiesRef.current.some(otherEnemy => { + if (otherEnemy === enemy) return false // Пропускаем сравнение с самим собой + return detectCollision({ ...enemy, x: newX, y: newY }, otherEnemy) + }) - return { ...enemy, x: newX, y: newY, direction: newDirection } + // Если нет коллизии, обновляем позицию врага + if (!hasCollision) { + enemy.direction = newDirection // Обновляем направление врага + return { ...enemy, x: newX, y: newY, direction: newDirection } + } + + // Если есть коллизия, возвращаем текущую позицию + return enemy }) } -export const respawnEnemies = (enemiesRef: React.MutableRefObject) => { - enemiesRef.current = initializeEnemies(5) +const isPositionOccupied = ( + position: { x: number; y: number }, + enemies: AbstractEntity[] +) => { + return enemies.some(enemy => + detectCollision({ ...enemy, x: position.x, y: position.y }, enemy) + ) +} + +const respawnEnemy = ( + enemy: AbstractEntity, + enemies: AbstractEntity[], + canvasWidth: number, + canvasHeight: number +) => { + let newPosition + do { + newPosition = { + x: Math.random() * canvasWidth, // Предполагается, что canvasWidth доступен + y: Math.random() * canvasHeight, // Предполагается, что canvasHeight доступен + } + } while (isPositionOccupied(newPosition, enemies)) + + return { ...enemy, ...newPosition } // Возвращаем врага с новой позицией +} + +// Пример использования respawnEnemy в вашем коде +export const respawnEnemies = ( + enemiesRef: React.MutableRefObject, + canvasRef: React.MutableRefObject +) => { + if (!canvasRef.current) { + return + } + const { width: canvasWidth, height: canvasHeight } = + canvasRef.current.getBoundingClientRect() + enemiesRef.current = enemiesRef.current.map(enemy => + respawnEnemy(enemy, enemiesRef.current, canvasWidth, canvasHeight) + ) } export const killEnemy = ( diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index a2ba21a..d7b80be 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -108,7 +108,7 @@ export const gameLoop = ( livesRef, handleGameOver, () => resetPlayerPosition(playerRef), - () => respawnEnemies(enemiesRef) + () => respawnEnemies(enemiesRef, canvasRef) ) } }) From e55a0ac8894fb51683f5dbe181157305b6e98c9f Mon Sep 17 00:00:00 2001 From: Daniil <77277774+shamemask@users.noreply.github.com> Date: Sat, 12 Oct 2024 02:09:44 +0300 Subject: [PATCH 09/13] =?UTF-8?q?[SOK-36]=20fix:=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B8=D0=BB=20=D1=80=D0=B0=D1=81=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D1=8F=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B9,=20?= =?UTF-8?q?=D0=B0=20=D1=82=D0=B0=D0=BA=D0=B6=D0=B5=20=D0=B8=D1=85=20=D1=80?= =?UTF-8?q?=D0=B0=D0=B7=D1=80=D1=83=D1=88=D0=B5=D0=BD=D0=B8=D0=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../client/src/components/Game/collision.tsx | 12 +++++ packages/client/src/components/Game/enemy.tsx | 20 ++++++--- .../client/src/components/Game/gameLoop.tsx | 6 ++- .../client/src/components/Game/obstacle.tsx | 44 ++++++++++++++++--- packages/client/src/components/Game/utils.tsx | 11 +++++ 5 files changed, 80 insertions(+), 13 deletions(-) diff --git a/packages/client/src/components/Game/collision.tsx b/packages/client/src/components/Game/collision.tsx index aba48a6..aa62ce3 100644 --- a/packages/client/src/components/Game/collision.tsx +++ b/packages/client/src/components/Game/collision.tsx @@ -35,3 +35,15 @@ export const detectBulletCollision = ( bullet.y + bullet.height > entity.y ) } + +export const detectObstacleCollision = ( + obstacle1: Obstacle, + obstacle2: Obstacle +): boolean => { + return ( + obstacle1.x < obstacle2.x + obstacle2.width && + obstacle1.x + obstacle1.width > obstacle2.x && + obstacle1.y < obstacle2.y + obstacle2.height && + obstacle1.y + obstacle1.height > obstacle2.y + ) +} diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index 7037715..032c7c1 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -1,5 +1,5 @@ import { getRandomEdgePosition } from './utils' -import { AbstractEntity, Enemy } from '@/components/Game/gameTypes' +import { AbstractEntity, Enemy, Obstacle } from '@/components/Game/gameTypes' import { createBullet } from '@/components/Game/bullet' import { detectCollision } from '@/components/Game/collision' @@ -24,7 +24,8 @@ export const initializeEnemies = (numberOfEnemies: number) => { export const updateEnemyPositions = ( player: AbstractEntity, - enemiesRef: React.MutableRefObject + enemiesRef: React.MutableRefObject, + obstacles: Obstacle[] ) => { enemiesRef.current = enemiesRef.current.map(enemy => { // Определяем разницу по X и Y @@ -65,14 +66,19 @@ export const updateEnemyPositions = ( const newY = enemy.y + stepY // Проверка столкновений с другими врагами - const hasCollision = enemiesRef.current.some(otherEnemy => { - if (otherEnemy === enemy) return false // Пропускаем сравнение с самим собой + const hasEnemyCollision = enemiesRef.current.some(otherEnemy => { + if (otherEnemy === enemy) return false return detectCollision({ ...enemy, x: newX, y: newY }, otherEnemy) }) + // Проверка коллизий с препятствиями + const hasObstacleCollision = obstacles.some(obstacle => { + return detectCollision({ ...enemy, x: newX, y: newY }, obstacle) + }) + // Если нет коллизии, обновляем позицию врага - if (!hasCollision) { - enemy.direction = newDirection // Обновляем направление врага + if (!hasEnemyCollision && !hasObstacleCollision) { + enemy.direction = newDirection return { ...enemy, x: newX, y: newY, direction: newDirection } } @@ -123,7 +129,7 @@ export const respawnEnemies = ( } export const killEnemy = ( - enemiesRef: React.MutableRefObject, + enemiesRef: React.MutableRefObject, enemy: AbstractEntity ) => { enemiesRef.current = enemiesRef.current.filter(e => e !== enemy) diff --git a/packages/client/src/components/Game/gameLoop.tsx b/packages/client/src/components/Game/gameLoop.tsx index d7b80be..efc6562 100644 --- a/packages/client/src/components/Game/gameLoop.tsx +++ b/packages/client/src/components/Game/gameLoop.tsx @@ -24,6 +24,7 @@ import { } from '@/components/Game/collision' import { updatePlayerAction } from '@/components/Game/controls' import { updateBullets } from '@/components/Game/bullet' +import { handleBulletObstacleCollisions } from '@/components/Game/obstacle' /** * Основной игровой цикл, который обновляет состояние игры и перерисовывает экран каждый кадр. @@ -49,7 +50,7 @@ export const gameLoop = ( clearCanvas(context) // Обновление позиций врагов - updateEnemyPositions(playerRef.current, enemiesRef) + updateEnemyPositions(playerRef.current, enemiesRef, obstaclesRef.current) if (!canvasRef.current) return const moveProps: ControlsProps = { playerRef, @@ -63,6 +64,9 @@ export const gameLoop = ( // Стрельба врагов каждые 2 секунды handleEnemyShooting(enemiesRef.current, bulletsRef) + // Обработка столкновений с препятствиями + handleBulletObstacleCollisions(bulletsRef.current, obstaclesRef.current) + bulletsRef.current = updateBullets( bulletsRef.current, canvasRef.current.width, diff --git a/packages/client/src/components/Game/obstacle.tsx b/packages/client/src/components/Game/obstacle.tsx index 3743838..dba6b8f 100644 --- a/packages/client/src/components/Game/obstacle.tsx +++ b/packages/client/src/components/Game/obstacle.tsx @@ -1,12 +1,46 @@ -import { Obstacle } from '@/components/Game/gameTypes' -import { getRandomEdgePosition } from '@/components/Game/utils' +import { AbstractEntity, Obstacle } from '@/components/Game/gameTypes' +import { getRandomPosition } from '@/components/Game/utils' +import { + detectCollision, + detectObstacleCollision, +} from '@/components/Game/collision' export const initializeObstacle = (): Obstacle[] => { const obstacles: Obstacle[] = [] - for (let i = 0; i < 10; i++) { - const { x, y } = getRandomEdgePosition(800, 600) + + while (obstacles.length < 20) { + const { x, y } = getRandomPosition(800, 600) const obstacle: Obstacle = { x, y, width: 50, height: 50 } - obstacles.push(obstacle) + + // Проверяем, нет ли коллизий с существующими препятствиями + const hasCollision = obstacles.some(existingObstacle => + detectObstacleCollision(obstacle, existingObstacle) + ) + + if (!hasCollision) { + obstacles.push(obstacle) // Добавляем препятствие, если нет коллизий + } } + return obstacles } + +export const handleBulletObstacleCollisions = ( + bullets: AbstractEntity[], + obstacles: Obstacle[] +) => { + bullets.forEach(bullet => { + // Проверка коллизий с препятствиями + obstacles.forEach(obstacle => { + if (detectCollision(bullet, obstacle)) { + // Логика для уничтожения пули, если она попала в препятствие + bullets.splice(bullets.indexOf(bullet), 1) // Пример, удаляем пулю, если попала в препятствие + killObstacle(obstacles, obstacle) + } + }) + }) +} + +const killObstacle = (obstacles: Obstacle[], obstacle: Obstacle) => { + obstacles.splice(obstacles.indexOf(obstacle), 1) +} diff --git a/packages/client/src/components/Game/utils.tsx b/packages/client/src/components/Game/utils.tsx index e8ef626..aaacbc7 100644 --- a/packages/client/src/components/Game/utils.tsx +++ b/packages/client/src/components/Game/utils.tsx @@ -21,6 +21,17 @@ export const getRandomEdgePosition = ( } } +export const getRandomPosition = ( + canvasWidth: number, + canvasHeight: number, + obstacleWidth = 50, // Ширина препятствия + obstacleHeight = 50 // Высота препятствия +): { x: number; y: number } => { + const x = Math.random() * (canvasWidth - obstacleWidth) // Генерируем случайное X + const y = Math.random() * (canvasHeight - obstacleHeight) // Генерируем случайное Y + return { x, y } +} + export const clearCanvas = (context: CanvasRenderingContext2D) => { context.clearRect(0, 0, context.canvas.width, context.canvas.height) } From e72592700d700eef50cdb4df59e174be080539b4 Mon Sep 17 00:00:00 2001 From: Iskandarov Timur Date: Sun, 13 Oct 2024 01:56:04 +0500 Subject: [PATCH 10/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=B0=D0=BD=D0=BC=D0=B8=D1=86=D0=B8=D1=8E=20=D0=B4?= =?UTF-8?q?=D0=B2=D0=B8=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D1=82=D0=B0=D0=BD?= =?UTF-8?q?=D0=BA=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/assets/images/sprites/enemy.png | Bin 0 -> 18293 bytes .../client/src/assets/images/sprites/tank.png | Bin 0 -> 18137 bytes packages/client/src/components/Game/enemy.tsx | 45 +++++++- .../client/src/components/Game/gameTypes.tsx | 8 ++ .../client/src/components/Game/player.tsx | 8 +- packages/client/src/components/Game/utils.tsx | 105 ++++++++---------- 6 files changed, 105 insertions(+), 61 deletions(-) create mode 100644 packages/client/src/assets/images/sprites/enemy.png create mode 100644 packages/client/src/assets/images/sprites/tank.png diff --git a/packages/client/src/assets/images/sprites/enemy.png b/packages/client/src/assets/images/sprites/enemy.png new file mode 100644 index 0000000000000000000000000000000000000000..44c31549c1221e63cb1a8b23caa7a4097eec40c7 GIT binary patch literal 18293 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
    Hsu(xU_72h2vW%u zB+1M~qY>PF&%Jp1$tTn_cRAM8H&I#I77^={V0$_-q-NSy%FVrEInB17 zq}i8Dj@Uk#TF~CviAIaeRu9u=n34qd$$s#bEnNt zUn(`eKIFU#9+%XFD7ofC-~7hZY*gul=NJT3i}FY9TNqTaIAYBPAB1kf+?3-Wv zsv3pVh%{4>*cSQu{qKH9)mM~-)O|5~?8v#rrEY0$sR?q=RV26`nGCCxb}^yX2-20{ zBJGoYD>Ifwpc+|mUQjy>jF%VjHdefTp9*?Q@LXmOI~K^y$p9@br3kB9=ve2N%gTlS ziNPRNrnqb(KiUHIRV%*vQ>@j=(8R1`>M9uNWTqDHlP2Li6eQ`CBSYrmz%!>#)l^G_ z>WANdShe28PFmeH^StmVgGr%uQEA%6lpNJ>j^_(AN)CE7K1RFS-B#w>YdgOhk(e&~ z@sEC}>L5OErJSUHEARE4zo6>=@OS!ETP{YBcPb(|=0`;z%kHe~DMsTW=!fxmX^e02(ba$46W$ak0eZvdvl? zYjB{7tGN=gq$H+BJ9AA~X@($GxJZA=UlJ}Ba;XhES2#%XMW6%xMW)%yc;x%@c~UE@C~}DCBBS9$Z;2 zY9A3=_hRWwCRvoQ#G)kh;PrWD)m^2t89oukD`Ay}6^xLVjhrO@QV}GXktrY~2sYoS zUn~ofGMN(vDwhHl$z#qErzVawb|Big*Ke(=u4bl-=TNtzL}X-bZV4(OkRqLCWO$_E zjDQpgev5F*92XH#akV6tAxcFQoWx|5FR$q}Be`^}O0-BOb6-|2zS>(VonmH?Ug|1B zVvgcYbH#kQEfaj-S$GUmTetGm}I$txYP}EVPBC@ zgUHb{Pdu)sd5AS5N?4{zh!rXQP_85OkT8SPO>K8k?#x$`i(FG?GRq)S-jz~WQ!*h^ z81yn5Cihp4kwHji@Jg6O#&B~j+w4+5i^LA2rZcls$c3cC#A8vl5YFtZv%h9gi1G;n zDw#R=jyZgq^$*LW65GBBd3W!;VlYESQSwUk0+OFmO8%K2X`X08R2#ktJ^KVqYUuoQAv zoJ)y)G#?s9prZ#7TAY$fm-{9QCuJa#ptOWWL>bFORk21TkzsBtk*<4<8ck8L=w-x} zMNa}i7Ho;dQad6S)kUdQkc%<68y%-|39I--r5x%{sMkUz?R8_O?u!!2^Aiy?TzK8W z_yrUu`eE}1U=Pqt_fy$eYzk2IybOUOH&<;bNb|I^@_N3QX5}*hDi%tC-<8qEX1A%? ztJ3M@{d6j+6ju{FNii8gCOoQsR-gPzb4d=!L@2s~Za-YX4p@Cbgd9#m&?vFgRV-;zuAPH0XPt4(M8Cn5(p3X z!A2N5JBhB9rnINbguZ#T758uOp~GKgNfI#RLOfPo0e8rPBrG`Dty%!;E(@4l+m|mYpYWCHT(eaC!Y|Bo*Z- zqml@c3i4`ZCl6&cB-1(AGqLWX)x`jX9w(RO)m$W-CTzr-6X9?ZzH~lOzDd&uXmOUPL^p*T8XD^}1^%WT}OdpSh|Qc3A|9C$IK?sSdVyf)0FQmjfTZB}G69 z{Ny058S0FnOGbedCX@89&xMe4`H;7`;A-hmZHsV}uFPvt`_?3*6t=X?C0J?I;vW^} z99h1>mklp1+*ljh3qA~`88UYWjK&Ba45?q4XEN695mF3m3KRCJ5R z1$<^m#I6P|P7gs_oI)mnYE;>Qsi7MgID_op!1uk}c z{MJ@{=;jojKFR)apv=WBTbVGF-^8@nM19ekde#BhQ*V! zHSj`3Xg&*YLpT$bA_q2fs8@|%h#!GIzpETApQ;vxHL2|kGEy2Zw2R1wL-m3l*tKpJFe3Ck0atN|zt&Pe?);!sg zlkgj5=SxK^8=%87LuVD_?u&fXus6S#e?3y~-T zd8yiT*T1|FMN1QXaf1BZlUDrE{Xtdvi-99Bd{s~c`ae7!#=7pjx}(oNW5e&?+gt{w zA)WMMSFaoI-k!jo^J#qjd8Vit&FW^7skenDGW-8{oC>p$Rc-5HTiBOWTPqBz%WN(~ zwOsg0<)*p(0lC~U2bV9t7)3BZE#q^8b;`>N1yS$mI_f!6lRsp-QQ} zSr*%0ddgh#Rb1NZa1l9*F2Ln;Pf(72>GJiLM!9s=aAA0WgZ6y=WvX@`-Kyb+PE85R z1Y=6;JAoBE(dKURK?KK#0Y5Xm|M5e>mp|#mcB+Q-?gqAyO#B+ z%A5~P&;O{vk#!W6vU>FzGw4VYY59s3b3&0a%#gazRm9yZmf;@0oQ@*K;tIh|BmKTzn03iDjp` z^tN7lF2bV1#m(PG=isuw4i^!aOU1=92bb|VxJ0YCte_=$ZsO8$8J)?6eP=YbFqihG z#c{DtS6IW1Yl6s@!Z(Kk5vv}egKjYW0DsEw^u zZRf^x>|Ql<{|Xb%(2xZYHbSPD%UlU5vTAcYW5qc0^y_9oQ5EJn`$HC_v*!GXfsn4= zt5>o)3;KovXlIPzZ$6yBKs1A|J_kH?#57BDH0Q+6Tobh5w)L{@0!`Q%PtBZ<%k7Kg zGG4{y{Fnv1E-{zE8ZMSu`-aTq(l!T|d_g8>nOtt)KrY5*<`S#wFQXRRy5bUZnXKh9 zSAWSZmP@HVmz|f0%S5!6%Pp6Xi^Vddzf908@_{=~noyK7TIor4JdJ4B)qd(`u=bjl-748h!Xl~Aqa0{FtU$ElW zKk#8S5lJ+!%#k{cFc3ArnV891ac-cA zS~`#a?N^idpME8SZ@vgTyhlt{8*Zg?NgEcl>$9(CWj!-hTt;FuxQs83%ilda1bKRy za9MT$tLw&%KqZ9V0mzw%PQgGX^e}@S-}7Lo`mA^jTb6%RG*8b z!>Ro*{OP^DhwhQ?Q4^2iG|z0-9RBHdX_lSuME_8r+!!=e*0fn!S-aBI_ib`ALOuOO zr7gpf5=-GgnAr;kjl4`DchQ~Nw0RS+*N5kyc?RR-Ay~6<_+92s>eE1F8M+NkZaX$~ zI2bTw8qn5Q#qtFU21b0yq`i3m?Fl?V4eWEe(AnfvK66lKfVfYsZWUWmBDmh$W^gf( z!@qp;BDu6%sSGb8m-Z?y8=2L)C@x8IG1$SK=x%n+%O!oOxU^lHTx4UkqH*zDR*M`} zb8%c+E^9B6i?~k0C7qh);+liYluDFTqG(E)^p_@>2onMS`4OOhSn~cUrlFWovYKX( zES6f^u47A=yRI27AZ}e( z$TOReVh~D$AMM!C?ozKcEgq~4_|$8FS()#?7RTY?BpNBh(IWo(=VAQ)Q(1GMB&l$UIyo%Up)u%3M;4%c%vp6iQjO-J(7p7j~q{ND{~o3;bC$)Inn@^$ZRe(>dR6dtwn34vzl58o-fSStg@O@w6wn9^M`?# z&SvoSJyCd_2LAd%;LbH}1~`HLiqzC`GT%l$LbSZdY6=W%`i>1=yl+PVKY1gA&pn#O z|M5r&pV%G1^~>j9`Y!E|>PcN?Pu7`5Ru80e8dGv+Tg;bH_goe=DrpOHkr|MRi~iQ- zBAE=Sp47E27v1C{pX+MdiFvrl1Ld$VmwfRm=r0X&k^ZtEmj?UGRl%h!8--HiT&gV? zZF@jtl*j1&?5)h9*xC@M-?{I6OljZWzKg!&Kwws`T^`T7p{s=T9nLx72;85%k-`rT z#<7z@roVi@9hPJ(aw!+mwlLoJ`AH-Ly>NMbYQ?W!Zi#@AnUC3c1WlPC{N(pKkY`Tb z=}jVGo4_A`yMQwz8a{G!6Yg5)nA4w?n{s+QjpIYQ$`(}@`O2CVVSJsD@PNyOTh|2O z&`Vft8N;7Gr;d=7W|PxgTGex8gv($!h2OpR+UMePxUhTO5|@kN5+j!wxg59a5ri@mNaU&I^QQK5*$jAXCA2Uvza~iSS~Mf> znS_Uxci|sDGLFwaoWM64@!bN6bz8$z!whLI|0yWpYYfj$ zj^e-nejmPZAdGCG_Peq>*MIq02>~YqOGXjjdo6)jX2$vFfg)utO+8nsr%dMC2mPw$ zQa39y=i&9a%yay;#KqEp1+&n5m2$Om(d%-leQ$BArRtPwII?v6$zxdzMRIs>mmB~7 zwq-avp2eR%KEleI(~!@EviSXf>xZW|f^UAtjnCfMiMyREu|B#Q8)K_+zjp=x{FgiM z<bgUxl(0`OZ_5T667**4RKjG1~ZSZSzYpZ z9ft-A7^jx}(2YStF)Jv-d_No0>9^udqV3%59Wka=JT|IfF^@ITzqMR3zQ>y;R|x|NWAek_ioLnYkM)fkld z+`dN3TrrrLWr<7u@^<55Y499{tB6Y@1NAHgBN$@o0zz@%Prk|ATQsa1Ek$zPYuYW? z-pMqbwYCzniSQYkagrw6+Top5kwhtu|8BPpTP&S0sHq=49>W**j^p3G7xnQgn-fe>)D0L}WH&q2WgW372U4+UD}(6DAk0qqxN7O>&VfE3X+Y z)LMr}W^;Mu+k?wzzG-q<|2E~a{7rC? z>j;-&c)xd_P0L~uv z%sSV=86WPxF^eB{CGgnE7(R4eGkWblHf9n9T^?2+#7~ZfacUxo_ik%LPm==!5xJoy zf~h^1i*=P7IYN{3=*aZW=aQ<9>m{{Gj@-x+=C=2Y8ZM>TTw*zOjcbVuU3s`Vbv0La zJ}yq%++2pPVlJap5Esiu`jRXh%+AFnYI1R1MO>WLGMCu=TxOP`OMyGE;4Iz7MGP?eB>JA(&9C_ zlosIPbI#3W*Hz8Mf6-iwGMCkDo@!r9ea zoVK?Im&*8DTH>;3E=9b3xfnCJ)L1aZI=h(H+u3|}F;A)m#66| zH%2pCchN;{vOI^`TWp9yy>g9knWOzhEODt<-Y)&6p4D$|Za>}rDFq|P*6T2(24D#m z*^NxBffUWIJLxZ;3+L!k`B5lX*f@*w^($rPau0s?GJ{4)6A+_u(Q{TLH>Q)DxaFsXzsX_sheoHDmEXD2!+??t4?SVv_wRRObteNwjs&9C1a4lV~k0mjNVrp7h?{= zm`jn=AHl9gE_l;iO4lHldc~L_M^g%>Xf)~$%`R%!a+ihx1ECit&0vz-HvKRtx8_Hx zkjI6Txv@?=wdZf&*n(wD#T`G_gz>N)BiTueX2KYmaN$h9A8XrP_^q8m`0ZvD4~Ams zYAzw4GQ-=thN(1OKZ9R#?SRKg;o2@9WO;H#FSR7$>SpucJ6@ikSKi-nV~(_sp8 zSujgn>RY^TcP^#350|+!Wn`NM@j3kq*DcmtD<3+}|I=eH*4H5FH;&S zyvDfX7T_{YC2_5D`N9HRrW8yxE$J)+{n0!vnJk|9GaWDOU#1>oSzSbHedX0LyxJGX z>UIX6bW>Y4xizN0boIdP7tY3U&sr~2Yj!nb@i|7qcQBabwp;M6XQL?IJ%A5xY{$(j zyclJrdjizV9d_lWo!J~jBvZuJE*C7#Nqk}7IG#SBp>p1-SeUx%|^Km)Klf90mr$d9_dNv*hyf z99+Iy?k{h3E_=x(H3t_tW;eNfVh%3a)+Xy7-6-5FOAmfyt30#WkDnb5;a7JW^y56U zz(_eXl*GEu592F+CA@exho?_O@Jnm#7)mma&%n`+#X(Nt~iNcT=|o-K~MCAgSyVn&Ta@@q1(Og4*lT z=`P^9bwFp(^j(K02<{{g&f8RgD#%=YK3|00Zk^gpJIB8d4MZ>;2euK~j$W4nGc&y= z^8&`>MTBCOnl{K{u1pem1 zYoCkT=EjB=t%gh1+xD-9W=Sk@smmo!E}yFAGLo|5d?@}lP*om~7fFsXazHto@A9GFljupSmqSt>iooMgtdvwGzF16hJe58PPu1(-@)T@Gm zBNIuCr;2JeVpp4&n$oT+9~vrP@PcVpNkn@`msPF(5kGcdG=|Ac30*!L+FLwuP-AZM zC-LGLdCH!RCYcl0FSpdq-OIUpgF4MXjXtnlJ=EI-!{FE|F$Fp5Ml(1uTvRu;WOX9r zX+OsUWP`!FTtl{zXKHQ!wBYm&HI&xv>5OqSFw`#TlSvbb?x3&Q)DEM1FU&ezWU0F1 z(tT}mL79sL3>VF%u*5}iae2((ce0vG^qS@pPSxi!MTZIuy1xYlyO|#!jsWlbTSkcE zVcfKR2wfeChSPalXwu1JFL$r@V}d&DuU{@;ojZ!PoxrDm#ew!FLkajorAT^PmuvW= zckw%ZM%b-U{MAcEWJ~hkc)!}Dcx`**n+A2XCE1o?Oyno=p}#k9e%$=5VDq5pZAY_n z_V3#I8Lqp~Z6X)#DPu&DRo_Z6=owFTiCgvSj5K1D&a3)Ft8%%!2Tnh zNTdgu7E55EBHFph#qS*Mz(k@S|LNtT!rAK8ab!42&1}WGc5lryck2~-yuvaDq9VBz z{^Z-h3n$7SP|FtG&B)qlW-{{cv(3I)!i56@!sH^_Sx<(@rHKDoxU43Z2d-`|5}=C; zmxsvZ_%s*8+k&jc-4GYe?u149%LKW6_1flAbO+gAJPmO%Y z)r2gS>P#-R`@xhgBujt+DNi$;`8e{CQS3Wo;G_QlJg_5;-@8AHLdJ(s#5HF{{_e@J z+6$z~$$Y-U0uNu(qN%km^dnU?dw~g$1_4h^XybQe#$t6<-xy9Y5}w6hy|-=7bI5XZ zmnVjQdb)%kyb2Y766evzHf9J!x7vYJN_C7mOSztgCx{ZcXz_7qsWA4PHMzW-Tt0P` za`^(ee19*>yOe1#+zclA+9dKFlTuN6j7eBdp$>kYxd9}=i z{l(rS{Uu;-N07@k#3gU{!*B*TC{4hciL3tdV3~`^QSmC{;`WvMON9$d!Qn&6+iJ=c zFEW!$-934WY=LgIvD0+J-`0cl$T{R=A^h8Gz~gU}@SAr=@!lKL2!#U3=4P8#XU6mR zlV9h_4V#+TC`F)r^*M8)mOSh1x9_y6$G6T`5*Y)Jyk^pLFdp`ayKGZID8<#9!=HVp zSogbj`l@ZMG5qB9EWY?O)I-Rn*u2efb*-X#s6$Jq)C$r@OQFcfz0C$k=PFoB%TWvu zAsZRTkIChUGMD#}%fwZ}p%j?3)u@%bl! zAMDL>beh18%*}@)o+%9}`FVK;i{GWA3I3_~%`GM-P4Im6czm!f04A!-mttiQ8kX}r z6)^I1Bscq^;qrcSAcZ5NaeU_chI+ubYA6G5_O@Q=?tt=1oeb}5yhHh178#Dz(VV7o z_dG+oG6|XqzjQSh5hSTcGLbL`4F@e6zYSK7P-${{OLO6?xoI|+lEp@|p90|W%Y&5J z@~rA8g@W0Mr@0_AeXd$AqI5<0glECfY2~%UZse3erE?jR%d6z_OQEZV3n(~wxO;UXnS zE=S4bv*a>R=3;TU;b>pMsIt#gpoOC9FVY9bMn`6GDJz)L{>hvsj+x9WzBQFhICORv zc|J=no8e0yZAmlB+r=6=VNFjmEmg$-$^7@GWpVtwdo$>C1=J%$(>arb)2bmFo;Jm1 zJ~S%EbeDC3hr}l`(^^#)V#7)kE=aGnu4{H>nS?#5mrpNim!KrHk2gs#V;lj@4kPah?x5S@0DisSy zww!G_thodgO^93yipxfF`Q$z1;(jY|DI%PY)^L&RL6IX+-eGo>BHJRJ#7fIGOQkDq z%w{t)xEOW0GzVK$1-iPs5e&9qVqzTQW202a%TY>9DuEiKsed(a(b#^c3`G7`e#e5A z)>gE&w=dXBGy9tMJX3Fl>T}QJLZ}WIr8_#i(9_eSG`BE`#iAG<8bUl0MjA%Y~Gtz?wQrxU@J7KA4!k%;*brFB#YkEmCL%kAXyp|=nhja;nF9ej1c8=%eL z51=U+gu~%bZjo4Y4h~ADAZZJ7p&qy~5s%xgRN5YoTTM`tz80WhPK=MK*bF*6O37H# zVH_Va-cnqiBo|YcQI2RlXu)ojlnem@@^Lun8X02M73tL^5sxVaL*=SHSIwo)V@oux zNje-&x=sqx^4{JlL8=svY=kt9(Sbgs0RRX8_!ytbg~#yRF#|81jN_h7=7zvx-aUUV zKJ}ey#VfzRcp-rW6fe&)y{18Y*%!`FpHIxf8`hb>KXPOej~vYLc=K<2pbgH>mFQU3 z%h9JpY1(O^W?Lvc9Fh8!U=0rrE>IJbONQ-M!pMqevNFos)cL$VAJ%Qyz)>zv&^;I% z>_letJaUZBJa?R2PW@uI{N(5)ete*sODpU&Z-tB0S(JiSZk==nvFM~ab_!K_L|TxG z6gk$yQO)IYA~Z2k^O@W0MV;iWQ+1bnT#2`crz5{x<_HAU4f19(6r|>6HKjO3?L>p7=5ve1sz&iv z`k0aMYJ^7zF@Ew8@`*4WKWyGLnT#f@H7gpj8jSFeeJ&OPGxKwqOr%ZKVOkSEyf3GI z+B|-=tl5ULUNe<}ltQtfVoWhpkc@=|$$ZkMjB# zZ4*U`g^dPP+UZ&q7&KY6YPD*Fa21zJVw|FI#>d7W7EUu|Ey?~gF?a!?GlwZh5j=k6 z7stiw_Mv&z7PKv21+UkqEc7gOLTg*Q;vjuwf^~Pf-Ks68Pn}Ti^$;yz%;+q$6pSfI zG9p=&f{8aGfHoIgy_=C8rXw32g2uxF%b}x4Ib|wJbnJwhcUjX-GYO z`&9L1Mr7rbhHj+*wZqZ10y=|F(MUwS<|_h86h2WY|`(S)&9qQ4u5x zqqI+~U0Yd@q@*RN&ikWh6X-wuDssvA73E@cy3w=kW=dK>St}loxt*lM5;97V+En{R zxk%kbkYunrby5V$S`nlLiv&PjJa;@1C!4kC?pa>*xxcBo=J&2;%aGi>9VZXGiqP4^ zFc=`el3X;K15GP7V#AJI@CE#+92977o^?*f-B6&#d+yv>^6^jsrR!e1js>HYL~}=I z9!9E6#D6IiZdF5{!4>b1B!t1~tvNTSWf50{ZxI-vwY8!4$&N}0*YlY7afaD}*N@T}cI zL2rY{>qR3)TqLn-BzP@WUN{tL?0a+{+>FtQfUa7-1|419HJ|(ZwU6iUF!s~R(OpLL zI0x$SgU^vm{)%x4Y`O()tJcHMdQ>mIk$TN6BKX4M{JC>V(PYzUG*V=sPST|k1tVbw zx7*dw_nPRo-h9t5qkqjt9RAruSCq@fdwvb8s0bS^HX0!-R<6L|LvN^iSuQoQWM#B! z=C(OSF?&R7b#-^quWk%XrGzKL7&~vl5B8nqm|tr}$Xbb`V`(I)QJ**-S@2u}VOf)d zDa5o|%=P%O@T?=Ot-Tx_HlVe&1&b+JX2v9#DHaT)`(g)AfstUHS`b?Mm7)lqa9O`$ zy^3Khri^f;Je!J;%ZV$(MdteSu3Uwl-sMFnylv7<*J!U5Uud?uF?iZcexVlt6b z3Z`dy4;8Q-4SEn{=vA(Ea@pD|%0(=$K%k;b7Otzr+Iq?H=#isHG|WZIOG06gnWy1_ zeq2;>2FYdoig3v@)B5^>1M0QdVoqyNE?wT7sbhi=F52qe)eq=;>Bi}Uwfp(+F8W%! zrZwOO)F~2KHj0IuUdTSHp>(10zBe2C=&GQ@bvBfXC!N8E7CH~VYw_nI4=!)E+s;hn zLTOj<2UGRbv}J~Fzt3RK*`hbLj$&pJKbU5)xJLUK`F>=&0ovgbv`j4*<31j@`;u@e zW}nlH()r5!R}~knlqP_irjNa(QphJbovQ;^GD06J5{W?_zFulwY6Y# zA1XS14XJf&I=a?U$gJn_`}w_*s*lJV8(725Y+7T!%8a72tk4E!9uVu9lDSlrk(1d% z;YWc$pg5BYuXjDa)Qh(dmjkS4a*0d%1WR1%mif5+H&N?SbLeR`pa1{>07*qoM6N<$ Ef`l5YNdN!< literal 0 HcmV?d00001 diff --git a/packages/client/src/assets/images/sprites/tank.png b/packages/client/src/assets/images/sprites/tank.png new file mode 100644 index 0000000000000000000000000000000000000000..56e0a69311a5a495dc9719bfcbf58fa612eb3d3a GIT binary patch literal 18137 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 Ef{ZVapa1{> literal 0 HcmV?d00001 diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index d966e0c..cdc68f3 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -20,6 +20,12 @@ export const initializeRandomEnemies = (numberOfEnemies: number) => { id: i, x, y, + animation: { + currentFrame: 0, + totalFrames: 4, + frameInterval: 10, + frameCount: 0, + } } initialEnemies.push(enemy) } @@ -28,9 +34,42 @@ export const initializeRandomEnemies = (numberOfEnemies: number) => { export const initializeCampanyEnemies = (): Enemy[] => { return [ - { ...enemyParams, id: 0, x: 50, y: 55 }, - { ...enemyParams, id: 1, x: 320, y: 250 }, - { ...enemyParams, id: 2, x: 715, y: 60 }, + { + ...enemyParams, + id: 0, + x: 50, + y: 55, + animation: { + currentFrame: 0, + totalFrames: 4, + frameInterval: 10, + frameCount: 0, + } + }, + { + ...enemyParams, + id: 1, + x: 320, + y: 250, + animation: { + currentFrame: 0, + totalFrames: 4, + frameInterval: 10, + frameCount: 0, + } + }, + { + ...enemyParams, + id: 2, + x: 715, + y: 60, + animation: { + currentFrame: 0, + totalFrames: 4, + frameInterval: 10, + frameCount: 0, + } + }, ] } diff --git a/packages/client/src/components/Game/gameTypes.tsx b/packages/client/src/components/Game/gameTypes.tsx index 961d701..89174e5 100644 --- a/packages/client/src/components/Game/gameTypes.tsx +++ b/packages/client/src/components/Game/gameTypes.tsx @@ -1,3 +1,10 @@ +interface AnimationParams { + currentFrame: number, + totalFrames: number, + frameInterval: number, + frameCount?: number, +} + export interface AbstractEntity { x: number y: number @@ -5,6 +12,7 @@ export interface AbstractEntity { height: number speed: number direction: { x: number; y: number } + animation: AnimationParams } export interface Enemy extends AbstractEntity { diff --git a/packages/client/src/components/Game/player.tsx b/packages/client/src/components/Game/player.tsx index e1f1f72..14ecfc4 100644 --- a/packages/client/src/components/Game/player.tsx +++ b/packages/client/src/components/Game/player.tsx @@ -6,8 +6,14 @@ export const PLAYER_DEFAULT_PARAMS = { y: 560, width: 70, height: 70, - speed: 3, + speed: 2, direction: { x: 0, y: 0 }, + animation: { + currentFrame: 0, // Текущий кадр спрайта + totalFrames: 4, // Общее количество кадров в спрайте + frameInterval: 10, // Интервал для смены кадра (в циклах или миллисекундах) + frameCount: 0, // Счетчик кадров для контроля интервала + } } export const resetPlayerPosition = ( diff --git a/packages/client/src/components/Game/utils.tsx b/packages/client/src/components/Game/utils.tsx index d83bca5..e1ddacc 100644 --- a/packages/client/src/components/Game/utils.tsx +++ b/packages/client/src/components/Game/utils.tsx @@ -1,5 +1,6 @@ -import enemiesSpritePath from '@/assets/images/sprites/enemy.svg' -import tankSpritePath from '@/assets/images/sprites/tank.svg' +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' @@ -36,41 +37,59 @@ export const clearCanvas = (context: CanvasRenderingContext2D) => { context.clearRect(0, 0, context.canvas.width, context.canvas.height) } -const tankSprite = new Image() -tankSprite.src = tankSpritePath - -let lastPlayerDirection = { x: 0, y: 0 } - -export const drawPlayer = ( - context: CanvasRenderingContext2D, - player: AbstractEntity +const darawTank = ( + sprite: HTMLImageElement, + context: CanvasRenderingContext2D, + data: AbstractEntity ) => { - let direction = { ...player.direction } - - const isPlayerIdle = direction.x === 0 && direction.y === 0 + const { animation } = data + const { direction } = data + const moovment = direction.x !== 0 ? data.x : data.y + const spriteSettings = { + width: 0, + height: 0, + sourceX: 0, + sourceY: 0 + } - if (isPlayerIdle) { - direction = lastPlayerDirection - } else { - lastPlayerDirection = direction + // Если смещение кратно 10 меняем кадр + if (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; + context.save() + context.translate(data.x + data.width / 2, data.y + data.height / 2) + context.rotate(Math.atan2(direction.x, -direction.y)) + context.drawImage( + sprite, + spriteSettings.sourceX, + spriteSettings.sourceY, + spriteSettings.width, + spriteSettings.height, + -data.width / 2, + -data.height / 2, + data.width, + data.height + ) - context.translate(player.x + player.width / 2, player.y + player.height / 2) + context.restore() +} - const angle = Math.atan2(direction.x, -direction.y) - context.rotate(angle) +const playerSprite = new Image() - context.drawImage( - tankSprite, - -player.width / 2, - -player.height / 2, - player.width, - player.height - ) +playerSprite.src = playerSpritePath - context.restore() +export const drawPlayer = ( + context: CanvasRenderingContext2D, + player: AbstractEntity +) => { + darawTank(playerSprite, context, player) } const enemiesSprite = new Image() @@ -83,35 +102,7 @@ export const drawEnemies = ( enemies: Enemy[] ) => { enemies.forEach(enemy => { - let direction = { ...enemy.direction } - - // Если враг не двигается, используем последнее направление - const isEnemyIdle = direction.x === 0 && direction.y === 0 - if (isEnemyIdle) { - direction = lastEnemyDirection[enemy.id] || { x: 1, y: 0 } // по умолчанию вправо - } else { - lastEnemyDirection[enemy.id] = direction // сохраняем последнее направление для врага - } - - context.save() - - // Перемещаем контекст на позицию врага - context.translate(enemy.x + enemy.width / 2, enemy.y + enemy.height / 2) - - // Вычисляем угол поворота на основе направления - const angle = Math.atan2(direction.x, -direction.y) - context.rotate(angle) - - // Отрисовываем спрайт врага с учётом его ширины и высоты - context.drawImage( - enemiesSprite, - -enemy.width / 2, - -enemy.height / 2, - enemy.width, - enemy.height - ) - - context.restore() + darawTank(enemiesSprite, context, enemy) }) } From 2b714b24a2ab8ee3b4c7e7f17b9d39825cc4acba Mon Sep 17 00:00:00 2001 From: Iskandarov Timur Date: Sun, 13 Oct 2024 01:57:48 +0500 Subject: [PATCH 11/13] Lint & Format --- packages/client/index.html | 2 +- packages/client/public/serviceWorker.js | 17 ++++---- packages/client/src/components/Game/enemy.tsx | 40 +++++++++---------- .../client/src/components/Game/gameTypes.tsx | 8 ++-- .../client/src/components/Game/player.tsx | 10 ++--- packages/client/src/components/Game/utils.tsx | 37 ++++++++--------- packages/client/src/pages/SignUp/SignUp.scss | 7 +++- ...vite.config.ts.timestamp-1728127189350.mjs | 32 +++++++-------- 8 files changed, 78 insertions(+), 75 deletions(-) diff --git a/packages/client/index.html b/packages/client/index.html index 34ac846..d7b32d0 100644 --- a/packages/client/index.html +++ b/packages/client/index.html @@ -3,7 +3,7 @@ - + diff --git a/packages/client/public/serviceWorker.js b/packages/client/public/serviceWorker.js index 7a7c762..00f3c3d 100644 --- a/packages/client/public/serviceWorker.js +++ b/packages/client/public/serviceWorker.js @@ -1,13 +1,12 @@ const CACHE_NAME = 'cache-data-v1' -const urlsToCache = [ - '/' -] +const urlsToCache = ['/'] -const networkFirst = async (request) => { +const networkFirst = async request => { const cache = await caches.open(CACHE_NAME) try { const response = await fetch(request) - const cachePutCondition = response && response.status === 200 && response.type === 'basic' + const cachePutCondition = + response && response.status === 200 && response.type === 'basic' if (cachePutCondition) { await cache.put(request, response.clone()) } @@ -25,21 +24,21 @@ const networkFirst = async (request) => { } } -self.addEventListener('install', (event) => { +self.addEventListener('install', event => { event.waitUntil( caches .open(CACHE_NAME) - .then((cache) => { + .then(cache => { return cache.addAll(urlsToCache) }) .then(() => self.skipWaiting()) - .catch((err) => { + .catch(err => { console.error('Cache installation failed:', err) }) ) }) -self.addEventListener('fetch', (event) => { +self.addEventListener('fetch', event => { const { request } = event const { url, method } = request diff --git a/packages/client/src/components/Game/enemy.tsx b/packages/client/src/components/Game/enemy.tsx index cdc68f3..20b2526 100644 --- a/packages/client/src/components/Game/enemy.tsx +++ b/packages/client/src/components/Game/enemy.tsx @@ -24,8 +24,8 @@ export const initializeRandomEnemies = (numberOfEnemies: number) => { currentFrame: 0, totalFrames: 4, frameInterval: 10, - frameCount: 0, - } + frameCount: 0, + }, } initialEnemies.push(enemy) } @@ -34,41 +34,41 @@ export const initializeRandomEnemies = (numberOfEnemies: number) => { export const initializeCampanyEnemies = (): Enemy[] => { return [ - { - ...enemyParams, - id: 0, - x: 50, + { + ...enemyParams, + id: 0, + x: 50, y: 55, animation: { currentFrame: 0, totalFrames: 4, frameInterval: 10, - frameCount: 0, - } + frameCount: 0, + }, }, - { - ...enemyParams, - id: 1, - x: 320, + { + ...enemyParams, + id: 1, + x: 320, y: 250, animation: { currentFrame: 0, totalFrames: 4, frameInterval: 10, - frameCount: 0, - } + frameCount: 0, + }, }, - { - ...enemyParams, - id: 2, - x: 715, + { + ...enemyParams, + id: 2, + x: 715, y: 60, animation: { currentFrame: 0, totalFrames: 4, frameInterval: 10, - frameCount: 0, - } + frameCount: 0, + }, }, ] } diff --git a/packages/client/src/components/Game/gameTypes.tsx b/packages/client/src/components/Game/gameTypes.tsx index 89174e5..7978ee5 100644 --- a/packages/client/src/components/Game/gameTypes.tsx +++ b/packages/client/src/components/Game/gameTypes.tsx @@ -1,8 +1,8 @@ interface AnimationParams { - currentFrame: number, - totalFrames: number, - frameInterval: number, - frameCount?: number, + currentFrame: number + totalFrames: number + frameInterval: number + frameCount?: number } export interface AbstractEntity { diff --git a/packages/client/src/components/Game/player.tsx b/packages/client/src/components/Game/player.tsx index 14ecfc4..92a46dc 100644 --- a/packages/client/src/components/Game/player.tsx +++ b/packages/client/src/components/Game/player.tsx @@ -9,11 +9,11 @@ export const PLAYER_DEFAULT_PARAMS = { speed: 2, direction: { x: 0, y: 0 }, animation: { - currentFrame: 0, // Текущий кадр спрайта - totalFrames: 4, // Общее количество кадров в спрайте - frameInterval: 10, // Интервал для смены кадра (в циклах или миллисекундах) - frameCount: 0, // Счетчик кадров для контроля интервала - } + currentFrame: 0, // Текущий кадр спрайта + totalFrames: 4, // Общее количество кадров в спрайте + frameInterval: 10, // Интервал для смены кадра (в циклах или миллисекундах) + frameCount: 0, // Счетчик кадров для контроля интервала + }, } export const resetPlayerPosition = ( diff --git a/packages/client/src/components/Game/utils.tsx b/packages/client/src/components/Game/utils.tsx index e1ddacc..a815663 100644 --- a/packages/client/src/components/Game/utils.tsx +++ b/packages/client/src/components/Game/utils.tsx @@ -38,8 +38,8 @@ export const clearCanvas = (context: CanvasRenderingContext2D) => { } const darawTank = ( - sprite: HTMLImageElement, - context: CanvasRenderingContext2D, + sprite: HTMLImageElement, + context: CanvasRenderingContext2D, data: AbstractEntity ) => { const { animation } = data @@ -49,34 +49,34 @@ const darawTank = ( width: 0, height: 0, sourceX: 0, - sourceY: 0 + sourceY: 0, } // Если смещение кратно 10 меняем кадр - if (moovment % animation.frameInterval === 0) { - animation.currentFrame = (animation.currentFrame + 1) % - animation?.totalFrames; + if (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; + 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) context.rotate(Math.atan2(direction.x, -direction.y)) context.drawImage( - sprite, - spriteSettings.sourceX, - spriteSettings.sourceY, - spriteSettings.width, - spriteSettings.height, + sprite, + spriteSettings.sourceX, + spriteSettings.sourceY, + spriteSettings.width, + spriteSettings.height, -data.width / 2, -data.height / 2, - data.width, - data.height - ) + data.width, + data.height + ) context.restore() } @@ -93,6 +93,7 @@ export const drawPlayer = ( } const enemiesSprite = new Image() + enemiesSprite.src = enemiesSpritePath const lastEnemyDirection: Record = {} diff --git a/packages/client/src/pages/SignUp/SignUp.scss b/packages/client/src/pages/SignUp/SignUp.scss index 27eae7d..46015ec 100644 --- a/packages/client/src/pages/SignUp/SignUp.scss +++ b/packages/client/src/pages/SignUp/SignUp.scss @@ -1,6 +1,11 @@ @import '../../scss/form-mixin'; .registration-page { - @include page-background-layout($background-image: ($img_registration-background, $img_page_background_left)); + @include page-background-layout( + $background-image: ( + $img_registration-background, + $img_page_background_left, + ) + ); @include form-styles(); } diff --git a/packages/client/vite.config.ts.timestamp-1728127189350.mjs b/packages/client/vite.config.ts.timestamp-1728127189350.mjs index 2be45ef..3b1c00d 100644 --- a/packages/client/vite.config.ts.timestamp-1728127189350.mjs +++ b/packages/client/vite.config.ts.timestamp-1728127189350.mjs @@ -1,30 +1,28 @@ // vite.config.ts -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; -import dotenv from "dotenv"; -import path from "path"; -var __vite_injected_original_dirname = "/home/user/pr/tanks/packages/client"; -dotenv.config(); +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import dotenv from 'dotenv' +import path from 'path' +var __vite_injected_original_dirname = '/home/user/pr/tanks/packages/client' +dotenv.config() var vite_config_default = defineConfig({ server: { port: Number(process.env.CLIENT_PORT) || 3e3, - host: "0.0.0.0", + host: '0.0.0.0', watch: { - usePolling: true - } + usePolling: true, + }, }, define: { - __SERVER_PORT__: process.env.SERVER_PORT + __SERVER_PORT__: process.env.SERVER_PORT, }, resolve: { alias: { - "@": path.resolve(__vite_injected_original_dirname, "src") - } + '@': path.resolve(__vite_injected_original_dirname, 'src'), + }, }, plugins: [react()], - envDir: "../../" -}); -export { - vite_config_default as default -}; + envDir: '../../', +}) +export { vite_config_default as default } //# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcudHMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvaG9tZS91c2VyL3ByL3RhbmtzL3BhY2thZ2VzL2NsaWVudFwiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9maWxlbmFtZSA9IFwiL2hvbWUvdXNlci9wci90YW5rcy9wYWNrYWdlcy9jbGllbnQvdml0ZS5jb25maWcudHNcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfaW1wb3J0X21ldGFfdXJsID0gXCJmaWxlOi8vL2hvbWUvdXNlci9wci90YW5rcy9wYWNrYWdlcy9jbGllbnQvdml0ZS5jb25maWcudHNcIjtpbXBvcnQgeyBkZWZpbmVDb25maWcgfSBmcm9tICd2aXRlJ1xuaW1wb3J0IHJlYWN0IGZyb20gJ0B2aXRlanMvcGx1Z2luLXJlYWN0J1xuaW1wb3J0IGRvdGVudiBmcm9tICdkb3RlbnYnXG5kb3RlbnYuY29uZmlnKClcbmltcG9ydCBwYXRoIGZyb20gJ3BhdGgnXG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICBzZXJ2ZXI6IHtcbiAgICBwb3J0OiBOdW1iZXIocHJvY2Vzcy5lbnYuQ0xJRU5UX1BPUlQpIHx8IDMwMDAsXG4gICAgaG9zdDogJzAuMC4wLjAnLFxuICAgIHdhdGNoOiB7XG4gICAgICB1c2VQb2xsaW5nOiB0cnVlLFxuICAgIH0sXG4gIH0sXG4gIGRlZmluZToge1xuICAgIF9fU0VSVkVSX1BPUlRfXzogcHJvY2Vzcy5lbnYuU0VSVkVSX1BPUlQsXG4gIH0sXG4gIHJlc29sdmU6IHtcbiAgICBhbGlhczoge1xuICAgICAgJ0AnOiBwYXRoLnJlc29sdmUoX19kaXJuYW1lLCAnc3JjJyksXG4gICAgfSxcbiAgfSxcbiAgcGx1Z2luczogW3JlYWN0KCldLFxuICBlbnZEaXI6ICcuLi8uLi8nLFxufSlcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBMlIsU0FBUyxvQkFBb0I7QUFDeFQsT0FBTyxXQUFXO0FBQ2xCLE9BQU8sWUFBWTtBQUVuQixPQUFPLFVBQVU7QUFKakIsSUFBTSxtQ0FBbUM7QUFHekMsT0FBTyxPQUFPO0FBSWQsSUFBTyxzQkFBUSxhQUFhO0FBQUEsRUFDMUIsUUFBUTtBQUFBLElBQ04sTUFBTSxPQUFPLFFBQVEsSUFBSSxXQUFXLEtBQUs7QUFBQSxJQUN6QyxNQUFNO0FBQUEsSUFDTixPQUFPO0FBQUEsTUFDTCxZQUFZO0FBQUEsSUFDZDtBQUFBLEVBQ0Y7QUFBQSxFQUNBLFFBQVE7QUFBQSxJQUNOLGlCQUFpQixRQUFRLElBQUk7QUFBQSxFQUMvQjtBQUFBLEVBQ0EsU0FBUztBQUFBLElBQ1AsT0FBTztBQUFBLE1BQ0wsS0FBSyxLQUFLLFFBQVEsa0NBQVcsS0FBSztBQUFBLElBQ3BDO0FBQUEsRUFDRjtBQUFBLEVBQ0EsU0FBUyxDQUFDLE1BQU0sQ0FBQztBQUFBLEVBQ2pCLFFBQVE7QUFDVixDQUFDOyIsCiAgIm5hbWVzIjogW10KfQo= From 5ea21e70cf01bb9d5dd22fa9f61279ef5dcf633e Mon Sep 17 00:00:00 2001 From: Iskandarov Timur Date: Sun, 13 Oct 2024 02:01:59 +0500 Subject: [PATCH 12/13] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=81=D0=BF=D1=80=D0=B0=D0=B9=D1=82=20=D1=81=D0=BD?= =?UTF-8?q?=D0=B0=D1=80=D1=8F=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/assets/images/sprites/Bullet.png | Bin 0 -> 1352 bytes packages/client/src/components/Game/utils.tsx | 23 ++++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) create mode 100644 packages/client/src/assets/images/sprites/Bullet.png diff --git a/packages/client/src/assets/images/sprites/Bullet.png b/packages/client/src/assets/images/sprites/Bullet.png new file mode 100644 index 0000000000000000000000000000000000000000..d2a21d507e724014f31f5368bdedcd365f916797 GIT binary patch literal 1352 zcmeAS@N?(olHy`uVBq!ia0vp^JU}eO!3HGrSK5O(Ea{HEjtmSN`?>!lvI6-E$sR$z z3=CCj3=9n|3=F@3LJcn%7)lKo7+xhXFj&oCU=S~uvn$XBD6uFt!ZXd+mq822=3roB z6k=covKWDofl-P9WEzmg5XLACXU8#WK-Dk-B@G#XB!dtTr?qD?ut3#U0BI2L0Ai4S zFddMXnOCCc=Nh6=W~^tbXK2X4z`TGFVc!BKxPG1m%y2fyY=-?2=Td+aXMsm#F#`j) zFbFd;%$g$sH2+LyNJL45ua8x7ey(0(N`6wRUPW#JNEd@mg%yyQn_7~nP?4LHS8P>b zs{~eI1!RMS^_3LBN=mYAl_Got6rA&mQWebf4D<|??6?#Z6l{u8(yW49+@RWlJX@uV zl9B=|ef{$Ca=mh6z5JqdeM3u2OML?)eIp}XpbFjM%Dj@q3f;V7Wr!g#b6ir3lZ!G7 zN;32F6hP)CCgqow*eWT3EP?}wJ4-Ut5H{r%L%jv`pgu@O-%!s$ADcFyXW{Nc(N&RH z0MP{Y6~rK&(8lIXJ;gL5%W5OB@CpDpv%!vz}9!Y0tIEK`)I(=XbF|mnZ;^kBm3S`+R>~Lg# zZ*ukZ{@8OqlbW~+!^@wY*;)QBJM2OJL~+^Y>o^MMmvjW!++0~-?I$y5S7-183%@fA z^De#(^Z9w{f%A&@ty8zWznZmm;-{tkViUbn=Ly|C$Jbslp>$&w--ngzh9+lEU%pte z@oibWmVa$y>K(=Rc10Uqg?+)dQ8sv#X~_0gHl3dt7AfHx@cew7phq(U5lRFy7bSlBAgyqcmfMr6IGa z>8H}jMu(eE{&ZzMiN2}x$|Ns1ey{3T1@@gXQ8xT5j(--P?vv#0rpxkNd_qiwOWn?n zt*@_pFt5KK@-Vml=*9K(1#;vjdj`ceOc2_*qc_qmMoT0hE;7)hY@hjstgR1rt@{=@ zC*<`1j&lb*4}NZYvF&!omGX}t|FX+EKg?UcHg}b}YEtFNK<6-F*PivW=AC`uWVha} zWAESE5{VTKkaEhHA4?lgV%%Jsl6>nClu+{ATPwL34G`S`K@ b^~3fz)t}#vh<)`8R04Rq`njxgN@xNA*s9B& literal 0 HcmV?d00001 diff --git a/packages/client/src/components/Game/utils.tsx b/packages/client/src/components/Game/utils.tsx index a815663..64c25cb 100644 --- a/packages/client/src/components/Game/utils.tsx +++ b/packages/client/src/components/Game/utils.tsx @@ -134,12 +134,31 @@ export const drawObstacles = ( }) } +const bulletSprite = new Image() + +bulletSprite.src = bulletSpritePath + export const drawBullets = ( context: CanvasRenderingContext2D, bullets: AbstractEntity[] ) => { - context.fillStyle = 'yellow' bullets.forEach(bullet => { - context.fillRect(bullet.x, bullet.y, bullet.width, bullet.height) + context.save() + context.translate(bullet.x + bullet.width / 2, bullet.y + bullet.height / 2) + context.rotate(Math.atan2(bullet.direction.x, -bullet.direction.y)) + + context.drawImage( + bulletSprite, + 0, + 0, + bulletSprite.width, + bulletSprite.height, + -bulletSprite.width / 2, + -bulletSprite.height / 2, + bulletSprite.width, + bulletSprite.height + ) + + context.restore() }) } From afe203f28251229a155d7b5f7076d1aab057713b Mon Sep 17 00:00:00 2001 From: Iskandarov Timur Date: Tue, 15 Oct 2024 06:51:11 +0500 Subject: [PATCH 13/13] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B8:=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BD=D0=B5=D1=81=D0=BA=D0=BE=D0=BB=D1=8C=D0=BA=D0=BE?= =?UTF-8?q?=20=D1=82=D0=B8=D0=BF=D0=BE=D0=B2=20=D0=BF=D1=80=D0=B5=D0=BF?= =?UTF-8?q?=D1=8F=D1=82=D1=81=D1=82=D0=B2=D0=B8=D0=B9=20-=20=D0=94=D0=BE?= =?UTF-8?q?=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF=D1=80=D0=B5=D0=BF=D1=8F?= =?UTF-8?q?=D1=82=D1=81=D1=82=D0=B2=D0=B8=D1=8F=D0=BC=20=D0=B7=D0=B4=D0=BE?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D1=8C=D0=B5=20-=20=D0=94=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20=D1=81=D0=BC=D0=B5=D0=BD=D1=8F=D0=B5=D0=BC?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D1=8C=20=D0=B2=D0=BD=D0=B5=D1=88=D0=BD=D0=BE?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B2=D0=B8=D0=B4=D0=B0=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D1=80=D0=B0=D0=B7=D1=80=D1=83=D1=88=D0=B0=D0=B5=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20-=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D1=8D=D1=84=D1=84=D0=B5=D0=BA=D1=82=D1=8B=20=D0=B2?= =?UTF-8?q?=D1=8B=D1=81=D1=82=D1=80=D0=B5=D0=BB=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=BF=D0=BE=D0=BF=D0=BE=D0=B4=D0=B0=D0=BD=D0=B8=D0=B9=20-=20?= =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=B2=D0=BE=D0=B7?= =?UTF-8?q?=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D1=83=D0=BA?= =?UTF-8?q?=D0=B0=D0=B7=D0=B0=D1=82=D1=8C=20=D0=BA=D0=BE=D0=BB=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B8=D0=B9=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BF=D1=80?= =?UTF-8?q?=D0=B5=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 zcmV<29V6n2P)001Zm1^@s6c`Wgm00009a7bBm000XU z000XU0RWnu7ytkO0drDELIAGL9O(c600d`2O+f$vv5yPFh3S-{~{Ay#IIa%r=&7ERkpUejIK;_uO;-=l``cOEiR{6A5%e zPRIa6q#=~(W2L9)PnLV>IFb0H#iR5WCRpi&9JeCV5Q?DC*$F+BjosyR9Efe<{%R8T z(+N3#Wk?xNkc2aJO*o5=6A=W`1;a8pA;+r>831)1_MfhwhEYGAPRM}d-`RE^vJVeE zJ~B^MQwLCGgFWx`V&R$!m5u{hNaRitjlzcx$6*5+wfVP1&7U?i24Vixpk3HK4`n#o zvmb1*JfCPgu4(jdB1gr5`afx8-C`>L7Ll*Ojsv-gXpBPiGzRyd zID8ppg+x@jfar?g5St$Zk$FVcTBCXNHp*kVDBi8e(!+`#)DJn!c>ax-SsiqKYyjK?EDL|a`~F7(Dm+b?foNtit?XC`S`dcDARhl z97X%D{teMG`V{1wL|+5N{Yn=Ecbm^mEq*VhtQHbgjohJ#>WM@bJ{yWI`Z^dANSuay zWo)ZaHatRRx=r6H8T0#Nt{SOgFM9{WB% zansFtGI*mRB34+~V3*3eF-Jdzflwu>3_IL!7U_+?pt8xBI zxNizd4M6!a+Pr~|p?U`UI^kh8hBA{(79^e)IVe`NQ^%B6F{+rxO(=)3 zJBHHzfd960HBSPZG{ge6D*;I*utCqpdw!F$SsXh?4<{7^+>_+j)h7cDE!K$JNvbH4 zEs-KeG!_C3zIZH|d%}bXRPIm%8g%_?Q*$aKN1ED~Kw45C1G zt3)f_4SC%ny%~!NH&^e7-;Iu65Ft7T$T)O4(Sgb#W|>=v3N@3FZ$z1hj&NJB!v(-M zK$ur#B^ejggwAZ}hy$0Z5v^$#W}A{Tz=ZD)A;M~gu^z!CE#;3XZtAb zu(#jCgUn1Lr2(l(vecaIrRE$zwRt@h^12Adn9UVSIHwR*eHQ~*urcgk=1N&y0Co!wO}CYonZg@()yzUVi4`ACxUjOdqY|7oc+U#ENwmy zCF~su8WAn_dcEYvfI94UvcevWVeC?3ho7WjKkl@XyDc)>s;F1)BEz^{S9O#gGfV=@ z5~6DrNh+{T9xF(a_!X3yE~iuE36YjV&>eqBh5@W)Ll2LlmJQkXxbNq1V4dX1K)w8>?IwN`Oot zj=(f6I@d!kwAZ>VZZIPcuQz?4gG>M>OdIV|bWEA(imj;FoQ%>@$Y?Ms+Vp^8k@IRw z3c3JKhHC*PL7TuNHfx$;1Y=+&QKERFtG~e@6m$}uv4mI9wCQN$iglPMbQE$b(G1jk z`HperXpr=%!{N9r%i|Gw5*VUW7>s!ROUqY2v~}m+rDH}H|4ktAVq6IbocjWX(rvb? znwpmq>GYOB){^=n@21^#E%CnI-zNa^b>+C}QNgs8XxK0d5F}7eQm5i<8MNpkFI2GG zFOyW%Y>cYj4Ra8at2|0I0)2Zbdf}JY zJsa(G_{mhc%z7LP8i7(Auu<`80dh}wV79`adoMyRd;=qm0muzt8eu?&%wsz{Ce@ph zm0;=}bQFT9d>QIq2zBdd%=MElnktP4*zfa^-EK2jq`NRHS^!X1a|}jBQH*xRTn-9* zT;yqs*g!k8PQjcxmuS;_gS4#y!1G}a#9RGY3vG7W2#?z>@VhY+Q~+!}M(Zzm!^i!$ zs;bIR5L;~mdwYMM z06fF^$0TFkTUuzPIahcoQKx`41R6gRtO9UJc8$dRRuW{5wE8S^d3kn=w0~uY$_p$s zYmSc&Zc9*OSmTc5VYn27Jr)C0iCNs|OzBAw&d5VuPoi9cE^{#RG5E4j*taaG>Xo5IXd$r*{U+jl%e}ZHKTTQ;c=&&_>cO8^^F%_e6b6-Pd??0k0(w$ zW9<`9KU;g!$;GQBMYs^Bi%mOLCPh_6RaFa7$@zt41r-Ml96F+=>lJ`t7npu@70S7M z%nad)XmlnVj#1NG@X9puj>HUhVr-mA*nm+}1-D?<;ybiBIuU@IHrG!I?yas2!@I{z z1Iz$qNhW9#GY^>XoytKBFl?Z4c<62nf`dPVRV_1GDVgB&`>hlJ^I7v{5?sg?mg|Ov zXpI5{b^y@}P~v+itQz#eAY=s@rgl1EOwYpc+A0n=3_xyx`H8w7kGLFj>WcDYj`LC4 z(eV4c2Dmy28m4d4H1VR8rvDl`Z7Sw=mtZ~*ZfBJog?G&7oYA-iPQf%l^WF7>P{(tF z!RXeVpPcNl+2%SO4wL=x`MRLrQt`&>-H^j<%*~rO>y?$2#&~Vpwl6t$Wchor4JP5R zl%HoN)HID!ijs}k<|q5=8rC1_{YT6OEHIhxC6qh)*n?8e+qg!Dw#3PkCsXn4ERtZ< zlN;Wzv!inafKl+o$qJP__EIm|!8+j&qhvz}gaHzRgRZLW4LBDmsbU(0=w?bS$x4l5 zd<-78=NO1=XnIDv$eIPaDWIFK1$*h-Hth2`NyG%2;w5j9g97jFE;= zhUCS(6QFuMQ2?$J0GSCLxF)M4x#4kwVbm=NV}Q8}{QzG)+9K8;oddKV;8E)P$_%5T?`yP!60;(xyUxsPJ$Ki@uJ!S$pjyYRw5fW32ZiCW_c%hDKNri zI4+r(p{LV(F}O2k%%<6~M@AJ=KSedeIGPQtC8;Kfz`Hc;x=s?srG>fWvFD$!tZc}f ze0BMXf4uV6%KKGazX!HfyLrOkmAb_ZVY?ZzM10|hj|Z5-n>l3)qxVKZ6iP%48sG=K$LbRNT~>abp>!os;>cEuMr2N%27o42 zquGvOPGf>_;RgAr%V;`Y`g7CA&f9kMdvPJ!vP-ebe?M4|3xJ3QE5N%nyk6?~_ARPS zU%Y5hRjS+LhI+I43YX}*_WR699#kJ&N(jLqTiM$~JpvHq5J3;@GO8eRV6E)B!s~Uwi?rh@tu{$|1cPfu z1hQ}}w%{H=T#9U%ivseRKCqyM<|JgYU{njP@)!Z@BA;x8Ks zR8**7ryn8ovNLn8dp)9I%jg=~{7P_uF~Q0YK4?MR&pI4VH4W+-=!b>REnC(`gAo7D zom`0lURBc+!|rMe{kERyAJHx~}idFn|)Nrw8WD-qG-R;Li| ze6Dj{u5gqWG5j%RuB;i5%$_vKm9Wzcgo31W#T!n0W7Xy*G&nh!?YI~SIFnO$kw_$# zOeU9BRP^qdAI5;s%_sj&578^ve~+&6*iE4Dd}GySgDn?%8=)3>kQ$0m3gq}EgL)8~ zmSDb%HWP$S_@M>!(8W+aey~kCj3C1vv&$W7lGWWInAxje15ueKq)$!Hpxz|cgcAD; z21vr`Eqk#X!_X3%5$Ms2%zo8dppG3K2@N(E^hUcu>v|zPWQT0{(R@b>nc(BPQpP%4 zsz5Cd1qZ+`_=dS~5^_2_9|%owjI7gLLO_*8PH7%7A3yJ`vm6#X-A6{WBwaV^fp=-_ z%h>vwL$6vj?dIPNB9zr8C(p+S%@Nb-Hn5?u>WXQ08idTdbY_7_BGS}wNf9oJ#}f`1 zJu|5fyT1EH;oU#c?N8i8=PZ607ZWB$a*J5?TC*gwBH+w+lVNRGkbWBrvrX=y%~l&p zGcG0}cPxU^rkZ0eVH?_(4gxcZ8WHAgI%tkG{;`eKXGek}5lErUnPb)$48l};n}j+| zRwHlE>2QEh@T5#0%Lq&Z!Ji_n07h~MAhgMp0s!HS8>txvA=4cY_dcf1*B>*h?Uw}2 zJ{^I)EdbJC+HqI{tdLIVn}Sx%y7rPYJV&g|CnM%BTt;b-axl8BEo6Yug5>M)o%<=P zEwpDK?dvR}s&ihqeU_kQr-=#ILS6$%1C^v=Wme~1cC_Fu$(V#cIP=soz5Ya_%ZnD6 zzQNVpZ?UlHPWur=E=LM$`1}Bs+@41{cjS^Z8V*KTgrpqsjeex%Y3Q@qZW85nB$fdQ z0fj8ZHY&TlfQDZmpt3VuWOsLYJ&(+*P<6d}O)NotF2M05rSD~G;xFgn4q>85XCVY| zp3R8P3S*#tbHm2;?)YjB(7Hr@Kk}K~+QtxT z_TR#DLJT)+O!0vf3JhaEs$D|y$91u`CC*Q>%Qy*<_~AZ0`_ zhGmXp+?5y<0jf#_^T~+kVGm}Vm9$hY>CTB9%jmggol#tmU^Hw+I4fG*0+UQI)Sl*s z3P(=X4u>R}Ug?9f4*xwC?58J(z#)5A_mbgem`G%#wBe`$skWxSwor6=l+;~Wh)y{> z7|<{p!Fr8uQmfq*eJw4(+6ZDIy76*`iDuq<8d7@${gD+cPkIa6Xm))Q`IN7Y7u>rl(R=KH%i zA%K#mm}V54X9S;vJ~@!22ukf+ZFCYqm_O4^UKBkB+phTy5~rL@LJ@KjVz;CUtJF4? zv;$tKIoMYpMJco?PKOY}+0&%ZLEU5;ei@p&6q~z$oA$i>BS8QaCV z^4Ep{B+EXq13tidlAWXxa5UKlCs;zFGafokaC^}RjcodJ2euFD*A$FNFb^@eB##hMjreK$;rJ!ylU z=XxAIsT%Kig5ybCVh6#TMkJgx04@&jwIYCB)2L7bZex3^KK%r*7J0IhP1j!unt1uh z_-Hbji(9gr>*f@RS3;Cpr_fp(#g+ z2b^F11*!oo{tLU0B4?{v6CDmvO6jFKoyI`l^awt7U=rO{#|RzvWOV!>QkfO2_s5iQ z>KnlF{ZPfRj4Fn;QXmQ`&S=tt1q&>5FP=G^oeb*aFtMHZm;wE_V|Lq_ z2g-&~%wacJ%!42j$Me|!Ldwm^aCFNJw+lNiY&$ZesGV#`_JB)v)sQO^g^@#ZpCEdJ z1|hc^)-TnpnL75r0scDzexJZ?LA$rWR(=8WPzF3)&dU?gA-6ar?e8f~kku4j3;oKA zRA9E7YYG57ipJt-uKG<^Tk^H#)zxW^IkFtK7y-(3pI|1n5nrKf1_>9A-EkMO-;g`U zM-~84t+683hXHPBGqMTPyaLAuN#BJbY-ltB)jDiH$!9U@XaWN^!bW47ZKieratJhQ zI?=MV{rtgAaPA0kbCWgkSY^HX9z4KlCV|bz!%)_ftG8qmAS_~#m6ix>(KrohKaUz-v0~U;$U)R9um;L57G}rX%Mi_+|nH$bF-dsf!E=Lo-284c@%qX4F zXbugvg$!GAdsb`sSHpMix$p|24=ei!UmZ5>@G+@*M2o>Br8$N4vo@rc(HDksp~1t< zyV6e*!ZD37-cw)>JAjB_200TONh)!{5tq_270lZGXw*LuD9u9#Wt@XN=XfaoW}DH8 zND~q=K*p_T;f#=JclIj+FWlHQb)}_4;QXZwYz9*1*WSDzPxn-7-0X`?oR7~lvch_M zFCjofkDz>jroEq&qAevq*nc!Dj&=0L!zw#>n(;GCcTYF5dTnlLrOYDG?rfJ4S6hUC zD+2aYH%Tc4N`u~SF-@ictKW=3TfBzO{O-h$KH0fx(#S6W?9b}Db#`ZaliR|o6~IIU zO_<#Ic~}?}3WazgvFZ1^Go~ym*|mQjTUo(=m@hr@egz)9)j`$HEYbi#V+kc2z0T2g zYJ&ZD;FT7VLaOm2pD5HScU!Ebo&q;v(!c@GLJ5lRN>bC35XGbL{I~*mrs8-9V$>F@ z-JPUSpoT{1yV_j_P@ImV_?<33Vkf5#QO5IeW;8Nc5@t6BoGX2vfyYQ!>9CR84r}ef zXP^aWh~#n1iEqNjb%&#uwkX~SsZOevyG6icWFwddxSNOEMF2X=fmdyhC3w!hgoy92 zXb9xj=(lT$z6}RdUbV5h>FiU+ZAM#W+wFERvS6@pI1)A52U9`e)!Q87<7b)mLSQ|< z2ZrO*W4G*nKu#p^xzvC&$ZhpKF^;%;A{C=G`?k^w)G(84LGFO|?SK~xTgk7R*%1|B z%Vad{d!bIAYa$gYFIkV+60#ABW)3uayz$zUVUMZE)3R%|=Y5m5(y~V|&96ZT?4KIg zTmM7m%IHi?jOR=hn!SnF!$4QfX*GgMhxZE3ZPiV)sUYY=WbmE27*!^TwKwsX0?N3t{i=(${V!kRu4;-qZISxiz(nMajpbDqV9f*5 zGq1LRB#bn^>qWHv54d;Dq5&+77^uvfH&3eBvGFoZ7rrQ(zi6n_NquV72ir3#VqyBE zaT74mQ*}vq!s!0UHB6~mwrt0selXuF0Qv8#U@+g*)61mT&)~2#Lz}y<5Y0;=jg}uZ zftgvxVq_&)uOjK&4E=XN?Z~l`x#jpXSMdq9KL@b?5E*WKO+Dy1@a9kyI-$fPd@WKGLa{m=DueKe9RyEY;VqnHes zaj6+ejTq(6!R)R=aHV=n7slh+%)i}T0bI60EjCmPBGxuo34*}W3pdq&<$E*lM(};I zYBF^bK((qfsQ7QjAl!8U`ij}7NH@cof5U`c`n$Am^;r|4uN#1rd(aU->Fr*=z}8D< zTC>5=ELb|uytaEwm)lpvr=R~zGhTc`dXS*`m$SG_Q;XUoL!V9Q6M(n!GCe<>o`!RpLs{(ZYw$aGq|O9=PgmX0^Y7MWl?At%I;egUvQ!ha(Ws`vW<8%M}F zjmJQH8^+dsk0D@6C**|mAVUVgjIg&^U`A&W|Ej-bcS8RE%Krgj|0W}lI3%9{0000< 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() + }) +}