Add in-game level editor with auto-discovered tile/entity palettes

Implements a full level editor that runs inside the game engine as an
alternative mode, accessible via --edit flag or E key during gameplay.
The editor auto-discovers available tiles from the tileset texture and
entities from a new central registry, so adding new game content
automatically appears in the editor without any editor-specific changes.

Editor features: tile painting (pencil/eraser/flood fill) across 3
layers, entity placement with drag-to-move, player spawn point tool,
camera pan/zoom, grid overlay, .lvl save/load, map resize, and test
play (P to play, ESC to return to editor).

Supporting changes:
- Entity registry centralizes spawn functions (replaces strcmp chain)
- Mouse input + raw keyboard access added to input system
- Camera zoom support for editor overview
- Zoom-aware rendering in tilemap, renderer, and sprite systems
- Powerup and drone sprites/animations wired up (were defined but unused)
- Bitmap font renderer for editor UI (4x6 pixel glyphs, no dependencies)
This commit is contained in:
Thomas
2026-02-28 20:24:43 +00:00
parent c66c12ae68
commit ea6e16358f
30 changed files with 4959 additions and 51 deletions

217
src/game/drone.c Normal file
View File

@@ -0,0 +1,217 @@
#include "game/drone.h"
#include "game/sprites.h"
#include "game/projectile.h"
#include "engine/renderer.h"
#include "engine/particle.h"
#include "engine/physics.h"
#include <stdlib.h>
#include <math.h>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
/* ── Stashed entity manager ────────────────────────── */
static EntityManager *s_em = NULL;
/* ── Helpers ───────────────────────────────────────── */
/* Find the active player entity */
static Entity *find_player(void) {
if (!s_em) return NULL;
for (int i = 0; i < s_em->count; i++) {
Entity *e = &s_em->entities[i];
if (e->active && e->type == ENT_PLAYER) return e;
}
return NULL;
}
/* Find nearest enemy within range */
static Entity *find_nearest_enemy(Vec2 from, float range) {
if (!s_em) return NULL;
Entity *best = NULL;
float best_dist = range * range;
for (int i = 0; i < s_em->count; i++) {
Entity *e = &s_em->entities[i];
if (!e->active || e->health <= 0) continue;
if (e->type != ENT_ENEMY_GRUNT && e->type != ENT_ENEMY_FLYER &&
e->type != ENT_TURRET) continue;
Vec2 epos = vec2(e->body.pos.x + e->body.size.x * 0.5f,
e->body.pos.y + e->body.size.y * 0.5f);
float dx = epos.x - from.x;
float dy = epos.y - from.y;
float dist2 = dx * dx + dy * dy;
if (dist2 < best_dist) {
best_dist = dist2;
best = e;
}
}
return best;
}
/* ── Callbacks ─────────────────────────────────────── */
static void drone_update(Entity *self, float dt, const Tilemap *map) {
(void)map;
DroneData *dd = (DroneData *)self->data;
if (!dd) return;
/* Countdown lifetime */
dd->lifetime -= dt;
if (dd->lifetime <= 0) {
/* Expiry: death particles and destroy */
Vec2 center = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
self->body.pos.y + self->body.size.y * 0.5f
);
particle_emit_spark(center, (SDL_Color){50, 200, 255, 255});
self->flags |= ENTITY_DEAD;
return;
}
/* Follow player — orbit around player center */
Entity *player = find_player();
if (!player) return;
Vec2 pcenter = vec2(
player->body.pos.x + player->body.size.x * 0.5f,
player->body.pos.y + player->body.size.y * 0.5f
);
dd->orbit_angle += DRONE_ORBIT_SPEED * dt;
if (dd->orbit_angle > 2.0f * (float)M_PI)
dd->orbit_angle -= 2.0f * (float)M_PI;
float target_x = pcenter.x + cosf(dd->orbit_angle) * DRONE_ORBIT_RADIUS;
float target_y = pcenter.y + sinf(dd->orbit_angle) * DRONE_ORBIT_RADIUS;
/* Smooth movement toward orbit position */
float lerp = 8.0f * dt;
if (lerp > 1.0f) lerp = 1.0f;
self->body.pos.x += (target_x - self->body.pos.x - self->body.size.x * 0.5f) * lerp;
self->body.pos.y += (target_y - self->body.pos.y - self->body.size.y * 0.5f) * lerp;
/* Shooting */
dd->shoot_cooldown -= dt;
if (dd->shoot_cooldown <= 0) {
Vec2 drone_center = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
self->body.pos.y + self->body.size.y * 0.5f
);
Entity *target = find_nearest_enemy(drone_center, DRONE_SHOOT_RANGE);
if (target) {
Vec2 tcenter = vec2(
target->body.pos.x + target->body.size.x * 0.5f,
target->body.pos.y + target->body.size.y * 0.5f
);
Vec2 dir = vec2(tcenter.x - drone_center.x, tcenter.y - drone_center.y);
/* Spawn projectile from drone, counted as player bullet */
projectile_spawn_def(s_em, &WEAPON_PLASMA,
drone_center, dir, true);
dd->shoot_cooldown = DRONE_SHOOT_CD;
/* Muzzle flash */
particle_emit_muzzle_flash(drone_center, dir);
}
}
/* Trail particles — subtle engine glow */
if ((int)(dd->lifetime * 10.0f) % 3 == 0) {
Vec2 center = vec2(
self->body.pos.x + self->body.size.x * 0.5f,
self->body.pos.y + self->body.size.y * 0.5f
);
ParticleBurst b = {
.origin = center,
.count = 1,
.speed_min = 5.0f,
.speed_max = 15.0f,
.life_min = 0.1f,
.life_max = 0.3f,
.size_min = 1.0f,
.size_max = 1.5f,
.spread = (float)M_PI * 2.0f,
.direction = 0,
.drag = 3.0f,
.gravity_scale = 0.0f,
.color = (SDL_Color){50, 180, 255, 150},
.color_vary = true,
};
particle_emit(&b);
}
/* Blink when about to expire (last 3 seconds) */
animation_update(&self->anim, dt);
}
static void drone_render(Entity *self, const Camera *cam) {
(void)cam;
DroneData *dd = (DroneData *)self->data;
if (!dd) return;
/* Blink when about to expire */
if (dd->lifetime < 3.0f) {
int blink = (int)(dd->lifetime * 8.0f);
if (blink % 2 == 0) return; /* skip rendering every other frame */
}
if (!g_spritesheet || !self->anim.def) return;
SDL_Rect src = animation_current_rect(&self->anim);
Body *body = &self->body;
/* Center sprite on hitbox */
float draw_x = body->pos.x + body->size.x * 0.5f - SPRITE_CELL * 0.5f;
float draw_y = body->pos.y + body->size.y * 0.5f - SPRITE_CELL * 0.5f;
Sprite spr = {
.texture = g_spritesheet,
.src = src,
.pos = vec2(draw_x, draw_y),
.size = vec2(SPRITE_CELL, SPRITE_CELL),
.flip_x = false,
.flip_y = false,
.layer = LAYER_ENTITIES,
.alpha = 255,
};
renderer_submit(&spr);
}
static void drone_destroy(Entity *self) {
free(self->data);
self->data = NULL;
}
/* ── Public API ────────────────────────────────────── */
void drone_register(EntityManager *em) {
entity_register(em, ENT_DRONE, drone_update, drone_render, drone_destroy);
s_em = em;
}
Entity *drone_spawn(EntityManager *em, Vec2 player_pos) {
Entity *e = entity_spawn(em, ENT_DRONE, player_pos);
if (!e) return NULL;
e->body.size = vec2(10.0f, 10.0f);
e->body.gravity_scale = 0.0f; /* floats freely */
e->health = 99; /* practically invulnerable */
e->max_health = 99;
e->damage = 0;
e->flags |= ENTITY_INVINCIBLE;
DroneData *dd = calloc(1, sizeof(DroneData));
dd->orbit_angle = 0.0f;
dd->shoot_cooldown = 0.5f; /* short initial delay */
dd->lifetime = DRONE_DURATION;
e->data = dd;
animation_set(&e->anim, &anim_drone);
/* Spawn burst */
particle_emit_spark(player_pos, (SDL_Color){50, 200, 255, 255});
printf("Drone deployed! (%0.0fs)\n", DRONE_DURATION);
return e;
}