Feat(Projectile) - Add Projectile class for enemy attacks; update enemy attack logic to create projectiles

This commit is contained in:
Félix MARQUET
2025-03-27 18:17:28 +01:00
parent 16b77f4bbf
commit 11770e0ca7
8 changed files with 128 additions and 25 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 190 KiB

18
main.py
View File

@@ -18,6 +18,7 @@ from src.constant import (
from src.Menu.Menu import Menu
from src.Menu.Leaderboard import Leaderboard
from src.Camera import Camera
from src.Entity.Projectile import Projectile
def main():
@@ -40,6 +41,7 @@ def main():
# Initialize game components
P1, PT1, platforms, all_sprites, background = initialize_game("map_test.json")
projectiles = pygame.sprite.Group()
# Main game loop
while True:
@@ -79,6 +81,9 @@ def main():
elif event.type == USEREVENT:
if event.action == "player_death":
current_state = MENU
if event.dict.get("action") == "create_projectile":
projectile = event.dict.get("projectile")
projectiles.add(projectile)
# Handle menu events
if current_state == MENU:
@@ -138,6 +143,19 @@ def main():
else:
sprite.update()
projectiles.update(WIDTH, HEIGHT, P1, camera)
for projectile in projectiles:
# Calculate position adjusted for camera (comme pour les autres sprites)
camera_adjusted_rect = projectile.rect.copy()
camera_adjusted_rect.x += camera.camera.x # SOUSTRAIT au lieu d'ajouter
camera_adjusted_rect.y += camera.camera.y # SOUSTRAIT au lieu d'ajouter
displaysurface.blit(projectile.surf, camera_adjusted_rect)
print(
f"Projectile: pos={projectile.pos}, rect={projectile.rect}, camera={camera.camera}"
)
# Display FPS and coordinates (fixed position UI elements)
fps = int(FramePerSec.get_fps())
fps_text = font.render(f"FPS: {fps}", True, (255, 255, 255))

View File

@@ -1,8 +1,8 @@
import pygame
import math
from src.Entity.Entity import Entity
from src.constant import FPS
from pygame.math import Vector2 as vec
from src.Entity.Projectile import Projectile
class Enemy(Entity):
@@ -37,7 +37,7 @@ class Enemy(Entity):
self.current_patrol_point = 0
self.patrol_points = enemy_data.get("patrol_points", [])
self.detection_radius = enemy_data.get("detection_radius", 200)
self.attack_interval = enemy_data.get("attack_interval", 2.0)
self.attack_interval = enemy_data.get("attack_interval", 0.3)
self.attack_range = enemy_data.get("attack_range", 300)
self.attack_timer = 0
self.direction = 1 # 1 = right, -1 = left
@@ -89,7 +89,6 @@ class Enemy(Entity):
if distance_to_player <= self.detection_radius:
self.detected_player = True
print("Player detected!")
direction = vec(player.pos.x - self.pos.x, player.pos.y - self.pos.y)
if direction.length() > 0:
@@ -113,11 +112,28 @@ class Enemy(Entity):
self.attack(player)
def attack(self, player):
"""Réalise une attaque sur le joueur"""
"""Do an attack action on the player"""
self.is_attacking = True
# TO DO: Implement attack logic based on enemy type
# Example: turret shoots a projectile
print("Enemy attacks player!")
# For turret-type enemies, create a projectile
if self.enemy_type == "turret":
# Calculate direction to player
direction = vec(player.pos.x - self.pos.x, player.pos.y - self.pos.y)
projectile = Projectile(
pos=vec(self.pos.x, self.pos.y),
direction=direction,
speed=self.speed,
damage=self.damage,
)
# Add projectile to the sprite group (to be placed in main.py)
pygame.event.post(
pygame.event.Event(
pygame.USEREVENT,
{"action": "create_projectile", "projectile": projectile},
)
)
def take_damage(self, amount):
"""Deal damage to the enemy and check if it should be destroyed"""

View File

@@ -1,5 +1,5 @@
from src.Entity.Entity import Entity
from src.constant import WIDTH, vec, ACC, FRIC, platforms, FPS
from src.constant import WIDTH, vec, ACC, FRIC, platforms, FPS, life_icon_width
from pygame import *
import pygame
import os
@@ -33,6 +33,7 @@ class Player(Entity):
self.invulnerable = False
self.invulnerable_timer = 0
self.invulnerable_duration = 1.5
self.life_icon = None
# Load images
self.load_images()
@@ -92,13 +93,26 @@ class Player(Entity):
dash_frame_height = dash_sheet.get_height()
for i in range(4): # Assuming 4 frames
for i in range(4):
frame = dash_sheet.subsurface(
(i * 2000, 0, dash_frame_height, dash_frame_height)
)
frame = pygame.transform.scale(frame, (80, 80))
self.dash_frames.append(frame)
# Load life icon
if os.path.isfile("assets/player/Sanic Head.png"):
self.life_icon = pygame.image.load(
"assets/player/Sanic Head.png"
).convert_alpha()
self.life_icon = pygame.transform.scale(
self.life_icon, (life_icon_width, life_icon_width)
)
else:
# Backup: use a red square
self.life_icon = pygame.Surface((life_icon_width, life_icon_width))
self.life_icon.fill((255, 0, 0))
except Exception as e:
print(f"Error loading player images: {e}")
@@ -196,7 +210,9 @@ class Player(Entity):
pygame.draw.rect(surface, (100, 100, 100), (x, y, bar_width, bar_height))
# Filled portion (based on cooldown progress)
pygame.draw.rect(surface, (58, 83, 200), (x, y, bar_width * cooldown_progress, bar_height))
pygame.draw.rect(
surface, (58, 83, 200), (x, y, bar_width * cooldown_progress, bar_height)
)
def update(self):
hits = pygame.sprite.spritecollide(self, platforms, False)
@@ -231,17 +247,26 @@ class Player(Entity):
print("Player died! Returning to menu...")
def draw_lives(self, surface):
"""Display remaning live on the top right of the screen"""
radius = 10
"""Draws the player's remaining lives as icons in the top right corner."""
spacing = 5
start_x = surface.get_width() - (self.max_lives * (radius * 2 + spacing))
start_y = 20
start_x = surface.get_width() - (self.max_lives * (life_icon_width + spacing))
start_y = 10
for i in range(self.max_lives):
color = (255, 0, 0) if i < self.lives else (100, 100, 100)
pygame.draw.circle(
surface,
color,
(start_x + i * (radius * 2 + spacing) + radius, start_y),
radius,
)
if i < self.lives:
# Vie active: afficher l'icône normale
surface.blit(
self.life_icon, (start_x + i * (life_icon_width + spacing), start_y)
)
else:
# Vie perdue: afficher l'icône grisée
grayscale_icon = self.life_icon.copy()
# Appliquer un filtre gris
for x in range(grayscale_icon.get_width()):
for y in range(grayscale_icon.get_height()):
color = grayscale_icon.get_at((x, y))
gray = (color[0] + color[1] + color[2]) // 3
grayscale_icon.set_at((x, y), (gray, gray, gray, color[3]))
surface.blit(
grayscale_icon, (start_x + i * (life_icon_width + spacing), start_y)
)

46
src/Entity/Projectile.py Normal file
View File

@@ -0,0 +1,46 @@
import pygame
from pygame.math import Vector2 as vec
class Projectile(pygame.sprite.Sprite):
def __init__(self, pos, direction, speed, damage, color=(0, 0, 255)):
super().__init__()
# Base attributes
self.pos = vec(pos)
self.direction = direction.normalize() if direction.length() > 0 else vec(1, 0)
self.speed = speed
self.damage = damage
# Create projectile surface
self.surf = pygame.Surface((10, 10))
self.surf.fill(color)
self.rect = self.surf.get_rect(center=(pos.x, pos.y))
def update(self, screen_width, screen_height, player=None, camera=None):
"""Move the projectile and check for collisions"""
# Movement of the projectile
self.pos += self.direction * self.speed
self.rect.center = (int(self.pos.x), int(self.pos.y))
# Check if projectile is out of screen
if camera:
# Screen position of the projectile = position + camera position
screen_x = self.pos.x + camera.camera.x
screen_y = self.pos.y + camera.camera.y
# Safety margin to avoid killing the projectile too early
margin = 50
if (
screen_x < -margin
or screen_x > screen_width + margin
or screen_y < -margin
or screen_y > screen_height + margin
):
self.kill()
# Check for collision with player
if player and self.rect.colliderect(player.rect):
player.take_damage(self.damage)
self.kill()

View File

@@ -126,6 +126,7 @@ class MapParser:
background = pygame.image.load(map_data["background"]).convert_alpha()
background = pygame.transform.scale(background, (WIDTH, HEIGHT))
self.background = background
print("Background image loaded")
else:
print(f"Background image not found: {map_data['background']}")
else:

View File

@@ -16,6 +16,7 @@ all_sprites = pygame.sprite.Group()
fullscreen = False
ORIGINAL_WIDTH = WIDTH
ORIGINAL_HEIGHT = HEIGHT
life_icon_width = 50
try:
font = pygame.font.SysFont("Arial", 20)

View File

@@ -1,11 +1,7 @@
import pygame
import sys
from pygame.locals import *
from src.Entity.Platform import Platform
from src.Entity.Player import Player
from src.constant import displaysurface, FramePerSec, font, FPS, platforms, all_sprites
from src.Map.parser import MapParser
from src.Camera import Camera
def initialize_game(map_file="map_test.json"):