diff --git a/Entities/Units/marine.py b/Entities/Units/marine.py index d47bda4..459af6a 100644 --- a/Entities/Units/marine.py +++ b/Entities/Units/marine.py @@ -11,7 +11,8 @@ class Marine(Entity): def update(self): self.centered_position = (self.iso_x, self.iso_y) - self.cast_rays_to_distance(6) + # Aggiorna il campo visivo dell'unità (fog of war) + self.cast_rays_to_distance(7) self.move() super().update() @@ -40,8 +41,8 @@ class Marine(Entity): # Controlla se l'unità èha raggiunto la cella di destinazione if self.target_cell != (self.x, self.y): # Contolla che la cella target non sia occupata da una unità che abbia quella cella come target - if self.engine.entities_positions.get(self.target_cell): - if self.engine.entities_positions.get(self.target_cell).target_cell == self.target_cell and self.engine.entities_positions.get(self.target_cell) != self: + if self.game.entities_positions.get(self.target_cell): + if self.game.entities_positions.get(self.target_cell).target_cell == self.target_cell and self.game.entities_positions.get(self.target_cell) != self: #calcola un nuovo target self.target_cell = self.graphics.get_next_cell(self.target_cell, self.position, ignore_units=True) if not self.target_cell: @@ -58,7 +59,7 @@ class Marine(Entity): self.next_cell = self.graphics.get_next_cell(self.next_cell, self.target_cell) # Se l'algoritmo ha efettivamente trovato una cella, occupa la posizione if self.next_cell: - self.engine.entities_positions[self.next_cell] = self + self.game.entities_positions[self.next_cell] = self # Se la cella di destinazione è occupata da un'altra entità, fermati else: self.action = "idle" @@ -115,23 +116,84 @@ class Marine(Entity): self.graphics.play_sound(f"marine/tmayes0{random.randint(0, 3)}.wav") self.game.cmd_sound_effects = True - def cast_rays_to_distance(self, distance): - # Cast rays to check for visibility - done_cells = {} + def cast_rays_to_distance(self, view_distance): + """ + Rivela tutte le celle nel raggio di vista dell'unità usando ray casting. + Le celle vengono illuminate indipendentemente dagli ostacoli, + ma quelle più lontane hanno maggiore ombreggiatura. + """ + # Pre-calcolo dei valori trigonometrici per ottimizzazione + if not hasattr(self, '_precomputed_angles'): + self._precompute_ray_angles() + + revealed_cells = {} + + # Lancia raggi in 18 direzioni (ogni 20 gradi) + for angle_idx, (sin_val, cos_val) in enumerate(self._ray_directions): + if angle_idx == 0: + self._cast_visibility_ray(angle_idx, sin_val, cos_val, view_distance+1, revealed_cells) + else: + self._cast_visibility_ray(angle_idx, sin_val, cos_val, view_distance, revealed_cells) + + def _precompute_ray_angles(self): + """Pre-calcola i valori sin/cos per evitare calcoli ripetuti ogni frame""" + self._ray_directions = [] for angle in range(0, 360, 20): - for i in range(distance, 0, -1): - ray_x = int(self.centered_position[0] + (distance -i) * int(self.engine.graphics.cell_size/3) * math.sin(math.radians(angle))) - ray_y = int(self.centered_position[1] - (distance - i)* int(self.engine.graphics.cell_size/6) * math.cos(math.radians(angle))) + radians = math.radians(angle) + self._ray_directions.append((math.sin(radians), math.cos(radians))) + self._precomputed_angles = True + + def _cast_visibility_ray(self, angle_idx, sin_val, cos_val, max_distance, revealed_cells): + """ + Lancia un singolo raggio di visibilità dalla posizione dell'unità. + Rivela tutte le celle lungo il percorso, applicando ombreggiatura crescente con la distanza. + """ + cell_size_x = int(self.engine.graphics.cell_size/3) + cell_size_y = int(self.engine.graphics.cell_size/6) + + # Calcola le posizioni lungo il raggio partendo dalla distanza massima verso il centro + for step in range(max_distance, 0, -1): + current_distance = max_distance - step + + # Calcola la posizione del punto sul raggio + ray_x = int(self.centered_position[0] + current_distance * cell_size_x * sin_val) + ray_y = int(self.centered_position[1] - current_distance * cell_size_y * cos_val) + + # Converte le coordinate schermo in coordinate griglia + grid_x, grid_y = self.graphics.inv_iso_transform(ray_x, ray_y) + + # Verifica che le coordinate siano valide + if self._is_valid_grid_position(grid_x, grid_y): + self._reveal_cell(grid_x, grid_y, step, revealed_cells) - v_x, v_y = self.graphics.inv_iso_transform(ray_x, ray_y) - if v_x>= 0 and v_x < len(self.engine.map[0]) and v_y >= 0 and v_y < len(self.engine.map): - if (v_x, v_y) not in done_cells.keys(): - done_cells.update({(v_x, v_y): self.game.map_shadow[v_y][v_x]}) - self.engine.map[v_y][v_x]["visited"] = True - self.game.map_shadow[v_y][v_x] = 0 - if i == 1: + # Disegna il raggio solo per il punto più distante (visualizzazione debug) + # if step == 1: + # self.graphics.draw_line( + # (self.centered_position[0], self.centered_position[1], ray_x, ray_y), + # color=(0, 255, 0, 255) + # ) + + def _is_valid_grid_position(self, grid_x, grid_y): + """Controlla se le coordinate della griglia sono valide""" + map_width = len(self.engine.map[0]) + map_height = len(self.engine.map) + return 0 <= grid_x < map_width and 0 <= grid_y < map_height - self.game.map_shadow[v_y][v_x] = max(done_cells.get((v_x, v_y), 0)-0.5, 0) - print(done_cells.get((v_x, v_y), 0)) - #self.graphics.draw_line((self.centered_position[0], self.centered_position[1], ray_x, ray_y), color=(0, 255, 0, 255)) - continue + def _reveal_cell(self, grid_x, grid_y, distance_step, revealed_cells): + """ + Rivela una singola cella della mappa e applica l'ombreggiatura appropriata. + Le celle più vicine (distance_step == 1) hanno ombra ridotta. + """ + cell_key = (grid_x, grid_y) + + # Salva il valore originale dell'ombra solo se non già processata + if cell_key not in revealed_cells: + revealed_cells[cell_key] = self.game.map_shadow[grid_y][grid_x] + # Marca la cella come visitata e rimuovi l'ombra base + self.engine.map[grid_y][grid_x]["visited"] = True + self.game.map_shadow[grid_y][grid_x] = 0 + + # Applica ombreggiatura speciale per le celle ai bordi del campo visivo + if distance_step == 1: + original_shadow = revealed_cells.get(cell_key, 0) + self.game.map_shadow[int(grid_y)][int(grid_x)] = max(original_shadow - 0.5, 0) diff --git a/enne2engine/engine.py b/enne2engine/engine.py index 7d71ce2..fd0a556 100644 --- a/enne2engine/engine.py +++ b/enne2engine/engine.py @@ -6,10 +6,10 @@ import json -class GameEngine(UserControls): +class GameEngine(UserControls,SDL2Renderer): def __init__(self, map, game): super().__init__() self.map = map self.game = game - self.graphics = SDL2Renderer(self) + self.graphics = SDL2Renderer(game, self) diff --git a/enne2engine/render.py b/enne2engine/render.py index 277279f..87c01a9 100644 --- a/enne2engine/render.py +++ b/enne2engine/render.py @@ -12,16 +12,17 @@ from .sdl2_utils.isogeometry import IsometricGeometry from .sdl2_utils.gui import SDL2Gui class SDL2Renderer(IsometricGeometry, SDL2Gui): - def __init__(self, engine): + def __init__(self, game, engine): self.engine = engine + self.game = game sdl2.ext.init() # Initialize SDL2 mixer sdlmixer.Mix_Init(sdlmixer.MIX_INIT_OGG | sdlmixer.MIX_INIT_MP3) sdlmixer.Mix_OpenAudio(44100, sdlmixer.MIX_DEFAULT_FORMAT, 2, 1024) sdlmixer.Mix_AllocateChannels(16) # Allocate channels for multiple sounds - - self.height = len(engine.map) - self.width = len(engine.map[0]) + + self.height = len(self.engine.map) + self.width = len(self.engine.map[0]) self.view_size = (800, 600) self.target_size = (800, 600) self.base_cell_size = 132 # Original/base cell size diff --git a/enne2engine/sdl2_utils/isogeometry.py b/enne2engine/sdl2_utils/isogeometry.py index ac83470..edbde2a 100644 --- a/enne2engine/sdl2_utils/isogeometry.py +++ b/enne2engine/sdl2_utils/isogeometry.py @@ -62,7 +62,7 @@ class IsometricGeometry: continue if not ignore_units: # Skip occupied cells - if self.engine.entities_positions.get((new_x, new_y)) is not None: + if self.game.entities_positions.get((new_x, new_y)) is not None: continue neighbors.append((new_x, new_y)) diff --git a/game_adv.py b/game_adv.py new file mode 100644 index 0000000..24cf292 --- /dev/null +++ b/game_adv.py @@ -0,0 +1,146 @@ +import json +import sys +import os + +from enne2engine import engine +from Entities.Units.marine import Marine + +class Game: + def __init__(self): + self.engine = engine.GameEngine(self.load_map(), self) + + self.frame_time = 0 + self.cursor_pos = (0, 0) + + self.load_assets() + self.entities = [] + self.entities_positions = {} + + def run(self): + running = True + # Set a custom scale if needed + self.engine.graphics.set_scaling_factor(1.0) # 50% scale + marine = Marine("knight", 0, 0, "idle", 1, 1, self) + self.entities.append(marine) + # self.entities.append(Marine("knight", 5, 5, "idle", 1, 1, self)) + # #5 more marines + # self.entities.append(Marine("knight", 0, 5, "idle", 1, 1, self)) + # self.entities.append(Marine("knight", 5, 5, "idle", 1, 1, self)) + # self.entities.append(Marine("knight", 1,1, "idle", 1, 1, self)) + # self.entities.append(Marine("knight", 2,2, "idle", 1, 1, self)) + # self.entities.append(Marine("knight", 3,3, "idle", 1, 1, self)) + while running: + self.cmd_sound_effects = False + # Start the frame timer + perf_counter = self.engine.graphics.get_perf_counter() + # Initialize the map shadow and entities positions + self.map_shadow = [ [1 for _ in range(len(self.engine.map[0]))] for _ in range(len(self.engine.map)) ] + self.entities_positions.clear() + for entity in self.entities: + self.entities_positions[entity.next_cell] = entity + self.entities_positions[(entity.x, entity.y)] = entity + + # Handle events + event = self.engine.graphics.handle_events() + if event: + #print(f"Event detected: {event}") + if event.startswith("MOUSEDOWN"): + print(f"Mouse down event: {event}") + print(f"Button pressed: {event[-1]}") + if event[-1] == "3": + print("Right mouse button pressed") + for entity in self.entities: + if entity.selected: + entity.set_target_cell(self.cursor_pos) + elif event.startswith("SELECTION"): + # Handle multiple unit selection + self.select_units_in_area(event) + elif event.startswith("MOUSEUP"): + if event[-1] == "1": + + self.select_entity_at_cursor() + self.engine.handle_events("keymap_game", event) + + running = False if event == "QUIT" else True + + self.engine.graphics.clear_screen() + + for entity in self.entities: + entity.update() + # Create the map background texture with tiles + self.engine.graphics.create_background(self.engine.map, "tiles", self.map_shadow) + self.cursor_pos = self.engine.graphics.draw_cursor() + + self.engine.graphics.render_background() + self.engine.graphics.render_sprites() + self.engine.graphics.draw_selection_rectangle() + self.engine.graphics.update_status(f"Frame time: {round(self.frame_time)}ms - FPS: {round(1000/self.frame_time if self.frame_time != 0 else 1)}") + self.engine.graphics.present_renderer() + self.frame_time = self.engine.graphics.get_frame_time(perf_counter) + self.engine.graphics.delay_frame(self.frame_time,50) + self.engine.graphics.quit() + + def set_cursor(self, x, y): + self.engine.graphics.cursor = (x, y) + + def load_assets(self): + self.engine.graphics.load_tilesheet("tiles", "assets/tiles/landscapeTiles_sheet.png") + for dir in os.listdir("assets/KnightBasic"): + for file in os.listdir(f"assets/KnightBasic/{dir}"): + if file.endswith(".json"): + self.engine.graphics.load_spritesheet(file[:-5].lower(), f"assets/KnightBasic/{dir}/{file}") + + def select_entity_at_cursor(self): + cursor_x, cursor_y = self.cursor_pos + print(f"Cursor position: {cursor_x}, {cursor_y}") + # First deselect all entities + for entity in self.entities: + entity.selected = False + + # Then select the entity at cursor position, if any + entity = self.entities_positions.get((cursor_x, cursor_y)) + if entity: + entity.select_unit() + print(f"Selected entity at cursor: {entity.asset} at position {entity.x}, {entity.y}") + else: + print("No entity selected at cursor position.") + + def select_units_in_area(self, selection_event): + """Select all units within the specified selection area.""" + # Parse selection coordinates + _, start_x, start_y, end_x, end_y = selection_event.split(":") + start_x, start_y, end_x, end_y = int(start_x), int(start_y), int(end_x), int(end_y) + + # Calculate selection rectangle in screen coordinates + min_x = min(start_x, end_x) + max_x = max(start_x, end_x) + min_y = min(start_y, end_y) + max_y = max(start_y, end_y) + + # First deselect all entities + for entity in self.entities: + entity.selected = False + + # Select entities within the rectangle + for entity in self.entities: + # Convert entity position to screen coordinates + screen_x, screen_y = self.engine.graphics.iso_transform(entity.x, entity.y) + + # Check if entity is within selection rectangle + if min_x <= screen_x <= max_x and min_y <= screen_y <= max_y: + entity.select_unit() + print(f"Selected entity in area: {entity.asset} at position {entity.x}, {entity.y}") + + def load_map(self): + # Load map from JSON file + try: + with open("assets/maps/map.json", "r") as map_file: + return json.load(map_file) + except (FileNotFoundError, json.JSONDecodeError) as e: + print(f"Error loading map file: {e}") + print("Exiting program.") + sys.exit(0) + +if __name__ == "__main__": + game = Game() + game.run() \ No newline at end of file