#include "game/laser_turret.h" #include "game/player.h" #include "game/sprites.h" #include "engine/core.h" #include "engine/physics.h" #include "engine/renderer.h" #include "engine/particle.h" #include "engine/camera.h" #include #include /* ═══════════════════════════════════════════════════ * Laser Turret * * Cycles through four states: * IDLE (brief) -> CHARGING (1.5s, thin flickering * indicator line) -> FIRING (2.5s, thick damaging * beam) -> COOLDOWN (2s, beam fades) -> IDLE ... * * The beam is a raycast from the turret center along * aim_angle until a solid tile or the map edge. * * Fixed variant: aim_angle set at spawn, never moves. * Tracking variant: aim_angle rotates toward the * player during IDLE and CHARGING, then locks when * FIRING begins. * ═══════════════════════════════════════════════════ */ static EntityManager *s_laser_em = NULL; /* ── Helpers ──────────────────────────────────────── */ static Entity *find_player(EntityManager *em) { for (int i = 0; i < em->count; i++) { Entity *e = &em->entities[i]; if (e->active && e->type == ENT_PLAYER) return e; } return NULL; } /* Raycast from (ox, oy) along (dx, dy) in tile steps. * Returns the world-space endpoint where the beam hits * a solid tile or the map boundary. Step size is one * pixel for accuracy. Max range prevents runaway. */ static void beam_raycast(const Tilemap *map, float ox, float oy, float dx, float dy, float *out_x, float *out_y) { float max_range = 800.0f; /* pixels */ float step = 1.0f; float x = ox, y = oy; float dist = 0.0f; while (dist < max_range) { x += dx * step; y += dy * step; dist += step; int tx = (int)(x / TILE_SIZE); int ty = (int)(y / TILE_SIZE); if (tx < 0 || ty < 0 || tx >= map->width || ty >= map->height) break; if (tilemap_is_solid(map, tx, ty)) break; } *out_x = x; *out_y = y; } /* ── Update ──────────────────────────────────────── */ static void laser_turret_update(Entity *self, float dt, const Tilemap *map) { LaserTurretData *ld = (LaserTurretData *)self->data; if (!ld) return; /* Death sequence */ if (self->flags & ENTITY_DEAD) { self->timer -= dt; if (self->timer <= 0) { particle_emit_death_puff(self->body.pos, (SDL_Color){255, 60, 60, 255}); entity_destroy(s_laser_em, self); } return; } /* Turret center for raycasting and aiming */ float cx = self->body.pos.x + self->body.size.x * 0.5f; float cy = self->body.pos.y + self->body.size.y * 0.5f; /* Tracking: rotate toward player during IDLE and CHARGING */ if (ld->tracking && (ld->state == LASER_IDLE || ld->state == LASER_CHARGING)) { Entity *player = find_player(s_laser_em); if (player && player->active && !(player->flags & ENTITY_DEAD)) { float px = player->body.pos.x + player->body.size.x * 0.5f; float py = player->body.pos.y + player->body.size.y * 0.5f; float target_angle = atan2f(py - cy, px - cx); /* Rotate toward target at limited speed */ float diff = target_angle - ld->aim_angle; /* Normalize to [-PI, PI] */ while (diff > 3.14159f) diff -= 6.28318f; while (diff < -3.14159f) diff += 6.28318f; float max_rot = LASER_TRACK_SPEED * dt; if (diff > max_rot) ld->aim_angle += max_rot; else if (diff < -max_rot) ld->aim_angle -= max_rot; else ld->aim_angle = target_angle; } } /* Update facing direction from aim */ float dx = cosf(ld->aim_angle); if (dx < 0) self->flags |= ENTITY_FACING_LEFT; else self->flags &= ~ENTITY_FACING_LEFT; /* Compute beam endpoint via raycast */ float dir_x = cosf(ld->aim_angle); float dir_y = sinf(ld->aim_angle); beam_raycast(map, cx, cy, dir_x, dir_y, &ld->beam_end_x, &ld->beam_end_y); /* Damage cooldown */ if (ld->damage_cd > 0) ld->damage_cd -= dt; /* State machine */ ld->timer -= dt; if (ld->timer <= 0) { switch (ld->state) { case LASER_IDLE: ld->state = LASER_CHARGING; ld->timer = LASER_CHARGE_TIME; break; case LASER_CHARGING: ld->state = LASER_FIRING; ld->timer = LASER_FIRE_TIME; ld->damage_cd = 0; break; case LASER_FIRING: ld->state = LASER_COOLDOWN; ld->timer = LASER_COOLDOWN_TIME; break; case LASER_COOLDOWN: ld->state = LASER_IDLE; ld->timer = 0.3f; /* brief idle before next cycle */ break; } } /* During FIRING: check beam-player overlap and deal damage. */ if (ld->state == LASER_FIRING && ld->damage_cd <= 0) { Entity *player = find_player(s_laser_em); if (player && player->active && !(player->flags & ENTITY_DEAD)) { /* Test player AABB against beam line (approximated as thin rect). */ float bx0 = cx, by0 = cy; float bx1 = ld->beam_end_x, by1 = ld->beam_end_y; float beam_half_w = 3.0f; /* beam thickness for collision */ /* Build an AABB enclosing the beam segment */ float min_x = (bx0 < bx1 ? bx0 : bx1) - beam_half_w; float min_y = (by0 < by1 ? by0 : by1) - beam_half_w; float max_x = (bx0 > bx1 ? bx0 : bx1) + beam_half_w; float max_y = (by0 > by1 ? by0 : by1) + beam_half_w; /* Quick AABB pre-filter */ Body *pb = &player->body; if (pb->pos.x + pb->size.x > min_x && pb->pos.x < max_x && pb->pos.y + pb->size.y > min_y && pb->pos.y < max_y) { /* Finer check: point-to-line distance for player center. */ float pcx = pb->pos.x + pb->size.x * 0.5f; float pcy = pb->pos.y + pb->size.y * 0.5f; float lx = bx1 - bx0, ly = by1 - by0; float len_sq = lx * lx + ly * ly; float dist_to_beam; if (len_sq < 0.01f) { dist_to_beam = sqrtf((pcx - bx0) * (pcx - bx0) + (pcy - by0) * (pcy - by0)); } else { float t = ((pcx - bx0) * lx + (pcy - by0) * ly) / len_sq; if (t < 0) t = 0; if (t > 1) t = 1; float closest_x = bx0 + t * lx; float closest_y = by0 + t * ly; dist_to_beam = sqrtf((pcx - closest_x) * (pcx - closest_x) + (pcy - closest_y) * (pcy - closest_y)); } /* Hit if player center is within half-size of beam */ float hit_radius = (pb->size.x + pb->size.y) * 0.25f + beam_half_w; if (dist_to_beam < hit_radius) { player->health -= LASER_DAMAGE; ld->damage_cd = LASER_DAMAGE_CD; particle_emit_spark(vec2(pcx, pcy), (SDL_Color){255, 80, 40, 255}); } } } } /* Sparks at beam endpoint during FIRING */ if (ld->state == LASER_FIRING) { if (rand() % 4 == 0) { particle_emit_spark(vec2(ld->beam_end_x, ld->beam_end_y), (SDL_Color){255, 100, 50, 255}); } } /* Animation */ if (ld->state == LASER_CHARGING || ld->state == LASER_FIRING) { animation_set(&self->anim, &anim_laser_turret_fire); } else { animation_set(&self->anim, &anim_laser_turret_idle); } animation_update(&self->anim, dt); } /* ── Render ──────────────────────────────────────── */ /* Draw the beam line from turret center to endpoint. * Uses raw SDL calls since renderer_draw_rect only handles * axis-aligned rectangles. */ static void draw_beam_line(const Camera *cam, float x0, float y0, float x1, float y1, int thickness, SDL_Color col) { Vec2 s0 = camera_world_to_screen(cam, vec2(x0, y0)); Vec2 s1 = camera_world_to_screen(cam, vec2(x1, y1)); SDL_SetRenderDrawBlendMode(g_engine.renderer, SDL_BLENDMODE_BLEND); SDL_SetRenderDrawColor(g_engine.renderer, col.r, col.g, col.b, col.a); /* Draw multiple lines offset perpendicular to simulate thickness. */ float dx = s1.x - s0.x; float dy = s1.y - s0.y; float len = sqrtf(dx * dx + dy * dy); if (len < 0.5f) return; /* Perpendicular direction */ float nx = -dy / len; float ny = dx / len; int half = thickness / 2; for (int i = -half; i <= half; i++) { float ox = nx * (float)i; float oy = ny * (float)i; SDL_RenderDrawLine(g_engine.renderer, (int)(s0.x + ox), (int)(s0.y + oy), (int)(s1.x + ox), (int)(s1.y + oy)); } SDL_SetRenderDrawBlendMode(g_engine.renderer, SDL_BLENDMODE_NONE); } static void laser_turret_render(Entity *self, const Camera *cam) { LaserTurretData *ld = (LaserTurretData *)self->data; if (!ld) return; Body *body = &self->body; float cx = body->pos.x + body->size.x * 0.5f; float cy = body->pos.y + body->size.y * 0.5f; /* Draw beam based on state */ if (ld->state == LASER_CHARGING) { /* Thin flickering indicator line */ float progress = 1.0f - (ld->timer / LASER_CHARGE_TIME); uint8_t alpha = (uint8_t)(80 + 100 * progress); /* Flicker effect: modulate alpha with a fast sine */ float flicker = sinf(ld->timer * 20.0f) * 0.3f + 0.7f; alpha = (uint8_t)((float)alpha * flicker); SDL_Color beam_col = {255, 60, 40, alpha}; draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y, 1, beam_col); } else if (ld->state == LASER_FIRING) { /* Bright thick beam — core + glow */ SDL_Color glow = {255, 80, 30, 100}; draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y, 5, glow); SDL_Color core = {255, 200, 150, 230}; draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y, 2, core); SDL_Color center = {255, 255, 220, 255}; draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y, 0, center); } else if (ld->state == LASER_COOLDOWN) { /* Fading beam */ float fade = ld->timer / LASER_COOLDOWN_TIME; uint8_t alpha = (uint8_t)(180 * fade); SDL_Color beam_col = {255, 80, 30, alpha}; draw_beam_line(cam, cx, cy, ld->beam_end_x, ld->beam_end_y, (int)(3.0f * fade), beam_col); } /* Draw turret body */ if (g_spritesheet && self->anim.def) { SDL_Rect src = animation_current_rect(&self->anim); /* Center the 16x16 sprite over the 14x14 hitbox */ Vec2 render_pos = vec2( body->pos.x - 1.0f, body->pos.y - 1.0f ); Sprite spr = { .texture = g_spritesheet, .src = src, .pos = render_pos, .size = vec2(SPRITE_CELL, SPRITE_CELL), .flip_x = (self->flags & ENTITY_FACING_LEFT) != 0, .flip_y = false, .layer = LAYER_ENTITIES, .alpha = 255, }; renderer_submit(&spr); } else { /* Colored rect fallback */ SDL_Color base_col; if (ld->state == LASER_FIRING) { base_col = (SDL_Color){200, 80, 50, 255}; } else if (ld->state == LASER_CHARGING) { base_col = (SDL_Color){180, 100, 60, 255}; } else { base_col = (SDL_Color){140, 100, 80, 255}; } renderer_draw_rect(body->pos, body->size, base_col, LAYER_ENTITIES, cam); /* Small dot indicating aim direction (only needed for fallback) */ float dot_dist = 10.0f; float dot_x = cx + cosf(ld->aim_angle) * dot_dist - 1.0f; float dot_y = cy + sinf(ld->aim_angle) * dot_dist - 1.0f; SDL_Color dot_col = {255, 50, 30, 255}; renderer_draw_rect(vec2(dot_x, dot_y), vec2(2, 2), dot_col, LAYER_ENTITIES, cam); } } /* ── Destroy ─────────────────────────────────────── */ static void laser_turret_destroy(Entity *self) { free(self->data); self->data = NULL; } /* ── Register ────────────────────────────────────── */ void laser_turret_register(EntityManager *em) { entity_register(em, ENT_LASER_TURRET, laser_turret_update, laser_turret_render, laser_turret_destroy); s_laser_em = em; } /* ── Spawn ───────────────────────────────────────── */ static Entity *laser_spawn_internal(EntityManager *em, Vec2 pos, bool tracking) { Entity *e = entity_spawn(em, ENT_LASER_TURRET, pos); if (!e) return NULL; e->body.size = vec2(LASER_WIDTH, LASER_HEIGHT); e->body.gravity_scale = 0.0f; e->health = LASER_HEALTH; e->max_health = LASER_HEALTH; e->damage = 0; /* damage comes from the beam, not contact */ e->timer = 0.3f; LaserTurretData *ld = calloc(1, sizeof(LaserTurretData)); if (!ld) { entity_destroy(em, e); return NULL; } ld->state = LASER_IDLE; ld->timer = 0.5f; /* brief idle before first charge */ ld->aim_angle = 3.14159f; /* default: aim left */ ld->damage_cd = 0; ld->tracking = tracking; e->data = ld; return e; } Entity *laser_turret_spawn(EntityManager *em, Vec2 pos) { return laser_spawn_internal(em, pos, false); } Entity *laser_turret_spawn_tracking(EntityManager *em, Vec2 pos) { return laser_spawn_internal(em, pos, true); }