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)
600 lines
20 KiB
C
600 lines
20 KiB
C
#include "game/hazards.h"
|
|
#include "game/sprites.h"
|
|
#include "game/projectile.h"
|
|
#include "engine/physics.h"
|
|
#include "engine/renderer.h"
|
|
#include "engine/particle.h"
|
|
#include <stdlib.h>
|
|
#include <math.h>
|
|
|
|
/* ════════════════════════════════════════════════════
|
|
* Shared helpers
|
|
* ════════════════════════════════════════════════════ */
|
|
|
|
/* Find the player entity in the manager */
|
|
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;
|
|
}
|
|
|
|
/* ════════════════════════════════════════════════════
|
|
* TURRET — floor/wall-mounted gun, shoots at player
|
|
* ════════════════════════════════════════════════════ */
|
|
|
|
static EntityManager *s_turret_em = NULL;
|
|
|
|
static void turret_update(Entity *self, float dt, const Tilemap *map) {
|
|
(void)map;
|
|
TurretData *td = (TurretData *)self->data;
|
|
if (!td) return;
|
|
|
|
/* Death: turrets can be destroyed */
|
|
if (self->flags & ENTITY_DEAD) {
|
|
self->timer -= dt;
|
|
if (self->timer <= 0) {
|
|
entity_destroy(s_turret_em, self);
|
|
}
|
|
return;
|
|
}
|
|
|
|
/* Flash timer for muzzle flash visual */
|
|
if (td->fire_flash > 0) {
|
|
td->fire_flash -= dt;
|
|
}
|
|
|
|
/* Look for player */
|
|
Entity *player = find_player(s_turret_em);
|
|
if (!player || !player->active || (player->flags & ENTITY_DEAD)) {
|
|
td->shoot_timer = TURRET_SHOOT_CD;
|
|
return;
|
|
}
|
|
|
|
/* Distance check */
|
|
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 tx = self->body.pos.x + self->body.size.x * 0.5f;
|
|
float ty = self->body.pos.y + self->body.size.y * 0.5f;
|
|
|
|
float dx = px - tx;
|
|
float dy = py - ty;
|
|
float dist = sqrtf(dx * dx + dy * dy);
|
|
|
|
/* Face toward player */
|
|
if (dx < 0) self->flags |= ENTITY_FACING_LEFT;
|
|
else self->flags &= ~ENTITY_FACING_LEFT;
|
|
|
|
if (dist < TURRET_DETECT_RANGE) {
|
|
td->shoot_timer -= dt;
|
|
if (td->shoot_timer <= 0 && s_turret_em) {
|
|
td->shoot_timer = TURRET_SHOOT_CD;
|
|
td->fire_flash = 0.15f;
|
|
|
|
/* Aim direction: normalize vector to player */
|
|
Vec2 dir;
|
|
if (dist > 0.01f) {
|
|
dir = vec2(dx / dist, dy / dist);
|
|
} else {
|
|
dir = vec2(1.0f, 0.0f);
|
|
}
|
|
|
|
/* Spawn projectile from turret barrel */
|
|
bool facing_left = (self->flags & ENTITY_FACING_LEFT) != 0;
|
|
float bx = facing_left ?
|
|
self->body.pos.x - 4.0f :
|
|
self->body.pos.x + self->body.size.x;
|
|
float by = self->body.pos.y + self->body.size.y * 0.3f;
|
|
|
|
projectile_spawn_dir(s_turret_em, vec2(bx, by), dir, false);
|
|
}
|
|
} else {
|
|
/* Reset cooldown when player leaves range */
|
|
if (td->shoot_timer < TURRET_SHOOT_CD * 0.5f)
|
|
td->shoot_timer = TURRET_SHOOT_CD * 0.5f;
|
|
}
|
|
|
|
/* Animation */
|
|
if (td->fire_flash > 0) {
|
|
animation_set(&self->anim, &anim_turret_fire);
|
|
} else {
|
|
animation_set(&self->anim, &anim_turret_idle);
|
|
}
|
|
animation_update(&self->anim, dt);
|
|
}
|
|
|
|
static void turret_render(Entity *self, const Camera *cam) {
|
|
(void)cam;
|
|
Body *body = &self->body;
|
|
|
|
if (g_spritesheet && self->anim.def) {
|
|
SDL_Rect src = animation_current_rect(&self->anim);
|
|
Vec2 render_pos = vec2(
|
|
body->pos.x - 1.0f,
|
|
body->pos.y
|
|
);
|
|
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 {
|
|
SDL_Color color = {130, 130, 130, 255};
|
|
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
|
|
}
|
|
}
|
|
|
|
static void turret_destroy(Entity *self) {
|
|
free(self->data);
|
|
self->data = NULL;
|
|
}
|
|
|
|
void turret_register(EntityManager *em) {
|
|
entity_register(em, ENT_TURRET, turret_update, turret_render, turret_destroy);
|
|
s_turret_em = em;
|
|
}
|
|
|
|
Entity *turret_spawn(EntityManager *em, Vec2 pos) {
|
|
Entity *e = entity_spawn(em, ENT_TURRET, pos);
|
|
if (!e) return NULL;
|
|
|
|
e->body.size = vec2(TURRET_WIDTH, TURRET_HEIGHT);
|
|
e->body.gravity_scale = 0.0f; /* turrets are stationary */
|
|
e->health = TURRET_HEALTH;
|
|
e->max_health = TURRET_HEALTH;
|
|
e->damage = 1; /* light contact damage — turret is a hostile machine */
|
|
|
|
TurretData *td = calloc(1, sizeof(TurretData));
|
|
td->shoot_timer = TURRET_SHOOT_CD;
|
|
td->fire_flash = 0.0f;
|
|
e->data = td;
|
|
|
|
/* Start with death timer if needed */
|
|
e->timer = 0.3f;
|
|
|
|
animation_set(&e->anim, &anim_turret_idle);
|
|
|
|
return e;
|
|
}
|
|
|
|
|
|
/* ════════════════════════════════════════════════════
|
|
* MOVING PLATFORM — travels back and forth,
|
|
* player rides on top
|
|
* ════════════════════════════════════════════════════ */
|
|
|
|
static EntityManager *s_mplat_em = NULL;
|
|
|
|
static void mplat_update(Entity *self, float dt, const Tilemap *map) {
|
|
(void)map;
|
|
MovingPlatData *md = (MovingPlatData *)self->data;
|
|
if (!md) return;
|
|
|
|
/* Advance phase (sine oscillation) */
|
|
float angular_speed = (MPLAT_SPEED / md->range) * 1.0f;
|
|
md->phase += angular_speed * dt;
|
|
if (md->phase > 2.0f * 3.14159265f)
|
|
md->phase -= 2.0f * 3.14159265f;
|
|
|
|
/* Calculate new position */
|
|
float offset = sinf(md->phase) * md->range;
|
|
Vec2 new_pos = vec2(
|
|
md->origin.x + md->direction.x * offset,
|
|
md->origin.y + md->direction.y * offset
|
|
);
|
|
|
|
/* Store velocity for player riding (carry velocity) */
|
|
self->body.vel.x = (new_pos.x - self->body.pos.x) / dt;
|
|
self->body.vel.y = (new_pos.y - self->body.pos.y) / dt;
|
|
|
|
self->body.pos = new_pos;
|
|
|
|
/* Check if player is standing on the platform */
|
|
Entity *player = find_player(s_mplat_em);
|
|
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
|
|
Body *pb = &player->body;
|
|
Body *mb = &self->body;
|
|
|
|
/* Player's feet must be at the platform top (within 3px) */
|
|
float player_bottom = pb->pos.y + pb->size.y;
|
|
float plat_top = mb->pos.y;
|
|
|
|
bool horizontally_on = (pb->pos.x + pb->size.x > mb->pos.x + 1.0f) &&
|
|
(pb->pos.x < mb->pos.x + mb->size.x - 1.0f);
|
|
bool vertically_close = (player_bottom >= plat_top - 3.0f) &&
|
|
(player_bottom <= plat_top + 4.0f);
|
|
bool falling_or_standing = pb->vel.y >= -1.0f;
|
|
|
|
if (horizontally_on && vertically_close && falling_or_standing) {
|
|
/* Carry the player */
|
|
pb->pos.x += self->body.vel.x * dt;
|
|
pb->pos.y = plat_top - pb->size.y;
|
|
pb->on_ground = true;
|
|
if (pb->vel.y > 0) pb->vel.y = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void mplat_render(Entity *self, const Camera *cam) {
|
|
(void)cam;
|
|
Body *body = &self->body;
|
|
|
|
if (g_spritesheet) {
|
|
SDL_Rect src = animation_current_rect(&self->anim);
|
|
Sprite spr = {
|
|
.texture = g_spritesheet,
|
|
.src = src,
|
|
.pos = body->pos,
|
|
.size = vec2(SPRITE_CELL, SPRITE_CELL),
|
|
.flip_x = false,
|
|
.flip_y = false,
|
|
.layer = LAYER_ENTITIES,
|
|
.alpha = 255,
|
|
};
|
|
renderer_submit(&spr);
|
|
} else {
|
|
SDL_Color color = {60, 110, 165, 255};
|
|
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
|
|
}
|
|
}
|
|
|
|
static void mplat_destroy(Entity *self) {
|
|
free(self->data);
|
|
self->data = NULL;
|
|
}
|
|
|
|
void mplat_register(EntityManager *em) {
|
|
entity_register(em, ENT_MOVING_PLATFORM, mplat_update, mplat_render, mplat_destroy);
|
|
s_mplat_em = em;
|
|
}
|
|
|
|
Entity *mplat_spawn(EntityManager *em, Vec2 pos) {
|
|
return mplat_spawn_dir(em, pos, vec2(1.0f, 0.0f)); /* default horizontal */
|
|
}
|
|
|
|
Entity *mplat_spawn_dir(EntityManager *em, Vec2 pos, Vec2 dir) {
|
|
Entity *e = entity_spawn(em, ENT_MOVING_PLATFORM, pos);
|
|
if (!e) return NULL;
|
|
|
|
e->body.size = vec2(MPLAT_WIDTH, MPLAT_HEIGHT);
|
|
e->body.gravity_scale = 0.0f;
|
|
e->health = 9999; /* indestructible */
|
|
e->max_health = 9999;
|
|
e->flags |= ENTITY_INVINCIBLE;
|
|
e->damage = 0;
|
|
|
|
MovingPlatData *md = calloc(1, sizeof(MovingPlatData));
|
|
md->origin = pos;
|
|
md->direction = dir;
|
|
md->range = MPLAT_RANGE;
|
|
md->phase = 0.0f;
|
|
e->data = md;
|
|
|
|
animation_set(&e->anim, &anim_platform);
|
|
|
|
return e;
|
|
}
|
|
|
|
|
|
/* ════════════════════════════════════════════════════
|
|
* FLAME VENT — floor grate with timed blue flames
|
|
* ════════════════════════════════════════════════════ */
|
|
|
|
static EntityManager *s_flame_em = NULL;
|
|
|
|
/* Damage cooldown tracking: use entity timer field */
|
|
#define FLAME_DAMAGE_CD 0.5f
|
|
|
|
static void flame_vent_update(Entity *self, float dt, const Tilemap *map) {
|
|
(void)map;
|
|
FlameVentData *fd = (FlameVentData *)self->data;
|
|
if (!fd) return;
|
|
|
|
/* Tick the phase timer */
|
|
fd->timer -= dt;
|
|
if (fd->timer <= 0) {
|
|
fd->active = !fd->active;
|
|
fd->timer = fd->active ? FLAME_ON_TIME : FLAME_OFF_TIME;
|
|
}
|
|
|
|
/* Warning flicker: 0.3s before flames ignite */
|
|
fd->warn_time = 0.0f;
|
|
if (!fd->active && fd->timer < 0.3f) {
|
|
fd->warn_time = fd->timer;
|
|
}
|
|
|
|
/* Animation */
|
|
if (fd->active) {
|
|
animation_set(&self->anim, &anim_flame_vent_active);
|
|
} else {
|
|
animation_set(&self->anim, &anim_flame_vent_idle);
|
|
}
|
|
animation_update(&self->anim, dt);
|
|
|
|
/* Damage player when flames are active */
|
|
if (fd->active) {
|
|
/* Damage cooldown */
|
|
if (self->timer > 0) {
|
|
self->timer -= dt;
|
|
}
|
|
|
|
Entity *player = find_player(s_flame_em);
|
|
if (player && player->active &&
|
|
!(player->flags & ENTITY_DEAD) &&
|
|
!(player->flags & ENTITY_INVINCIBLE) &&
|
|
self->timer <= 0) {
|
|
/* Check overlap with flame area (the full sprite, not just base) */
|
|
if (physics_overlap(&self->body, &player->body)) {
|
|
player->health -= FLAME_DAMAGE;
|
|
if (player->health <= 0)
|
|
player->flags |= ENTITY_DEAD;
|
|
self->timer = FLAME_DAMAGE_CD;
|
|
|
|
/* Blue flame hit particles */
|
|
Vec2 hit_center = vec2(
|
|
player->body.pos.x + player->body.size.x * 0.5f,
|
|
player->body.pos.y + player->body.size.y * 0.5f
|
|
);
|
|
SDL_Color flame_col = {68, 136, 255, 255};
|
|
particle_emit_spark(hit_center, flame_col);
|
|
}
|
|
}
|
|
|
|
/* Emit ambient flame particles */
|
|
if ((int)(fd->timer * 10.0f) % 3 == 0) {
|
|
Vec2 origin = vec2(
|
|
self->body.pos.x + self->body.size.x * 0.5f,
|
|
self->body.pos.y + 2.0f
|
|
);
|
|
ParticleBurst burst = {
|
|
.origin = origin,
|
|
.count = 2,
|
|
.speed_min = 20.0f,
|
|
.speed_max = 50.0f,
|
|
.life_min = 0.1f,
|
|
.life_max = 0.3f,
|
|
.size_min = 1.0f,
|
|
.size_max = 2.5f,
|
|
.spread = 0.4f,
|
|
.direction = -1.5708f, /* upward */
|
|
.drag = 0.0f,
|
|
.gravity_scale = -0.3f, /* float upward */
|
|
.color = {100, 150, 255, 255},
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&burst);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void flame_vent_render(Entity *self, const Camera *cam) {
|
|
(void)cam;
|
|
FlameVentData *fd = (FlameVentData *)self->data;
|
|
Body *body = &self->body;
|
|
|
|
if (g_spritesheet && self->anim.def) {
|
|
SDL_Rect src = animation_current_rect(&self->anim);
|
|
Sprite spr = {
|
|
.texture = g_spritesheet,
|
|
.src = src,
|
|
.pos = body->pos,
|
|
.size = vec2(SPRITE_CELL, SPRITE_CELL),
|
|
.flip_x = false,
|
|
.flip_y = false,
|
|
.layer = LAYER_ENTITIES,
|
|
.alpha = 255,
|
|
};
|
|
|
|
/* Warning flicker: blink the sprite when about to ignite */
|
|
if (fd && fd->warn_time > 0) {
|
|
int blink = (int)(fd->warn_time * 20.0f) % 2;
|
|
spr.alpha = blink ? 180 : 255;
|
|
}
|
|
|
|
renderer_submit(&spr);
|
|
} else {
|
|
SDL_Color color = fd && fd->active ?
|
|
(SDL_Color){68, 136, 255, 255} :
|
|
(SDL_Color){80, 80, 80, 255};
|
|
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
|
|
}
|
|
}
|
|
|
|
static void flame_vent_destroy(Entity *self) {
|
|
free(self->data);
|
|
self->data = NULL;
|
|
}
|
|
|
|
void flame_vent_register(EntityManager *em) {
|
|
entity_register(em, ENT_FLAME_VENT, flame_vent_update, flame_vent_render, flame_vent_destroy);
|
|
s_flame_em = em;
|
|
}
|
|
|
|
Entity *flame_vent_spawn(EntityManager *em, Vec2 pos) {
|
|
Entity *e = entity_spawn(em, ENT_FLAME_VENT, pos);
|
|
if (!e) return NULL;
|
|
|
|
e->body.size = vec2(FLAME_WIDTH, FLAME_HEIGHT);
|
|
e->body.gravity_scale = 0.0f;
|
|
e->health = 9999;
|
|
e->max_health = 9999;
|
|
e->flags |= ENTITY_INVINCIBLE;
|
|
e->damage = FLAME_DAMAGE;
|
|
|
|
FlameVentData *fd = calloc(1, sizeof(FlameVentData));
|
|
fd->active = false;
|
|
fd->timer = FLAME_OFF_TIME;
|
|
fd->warn_time = 0.0f;
|
|
e->data = fd;
|
|
e->timer = 0.0f; /* damage cooldown */
|
|
|
|
animation_set(&e->anim, &anim_flame_vent_idle);
|
|
|
|
return e;
|
|
}
|
|
|
|
|
|
/* ════════════════════════════════════════════════════
|
|
* FORCE FIELD — energy barrier that toggles on/off
|
|
* ════════════════════════════════════════════════════ */
|
|
|
|
static EntityManager *s_ffield_em = NULL;
|
|
|
|
static void force_field_update(Entity *self, float dt, const Tilemap *map) {
|
|
(void)map;
|
|
ForceFieldData *fd = (ForceFieldData *)self->data;
|
|
if (!fd) return;
|
|
|
|
/* Tick the phase timer */
|
|
fd->timer -= dt;
|
|
if (fd->timer <= 0) {
|
|
fd->active = !fd->active;
|
|
fd->timer = fd->active ? FFIELD_ON_TIME : FFIELD_OFF_TIME;
|
|
}
|
|
|
|
/* Animation */
|
|
if (fd->active) {
|
|
animation_set(&self->anim, &anim_force_field_on);
|
|
} else {
|
|
animation_set(&self->anim, &anim_force_field_off);
|
|
}
|
|
animation_update(&self->anim, dt);
|
|
|
|
/* When active: block player and deal damage */
|
|
if (fd->active) {
|
|
Entity *player = find_player(s_ffield_em);
|
|
if (player && player->active && !(player->flags & ENTITY_DEAD)) {
|
|
Body *pb = &player->body;
|
|
Body *fb = &self->body;
|
|
|
|
if (physics_overlap(pb, fb)) {
|
|
/* Push player out of the force field */
|
|
float player_cx = pb->pos.x + pb->size.x * 0.5f;
|
|
float field_cx = fb->pos.x + fb->size.x * 0.5f;
|
|
|
|
if (player_cx < field_cx) {
|
|
/* Push left */
|
|
pb->pos.x = fb->pos.x - pb->size.x;
|
|
if (pb->vel.x > 0) pb->vel.x = 0;
|
|
} else {
|
|
/* Push right */
|
|
pb->pos.x = fb->pos.x + fb->size.x;
|
|
if (pb->vel.x < 0) pb->vel.x = 0;
|
|
}
|
|
|
|
/* Damage with cooldown */
|
|
if (!(player->flags & ENTITY_INVINCIBLE) && self->timer <= 0) {
|
|
player->health -= FFIELD_DAMAGE;
|
|
if (player->health <= 0)
|
|
player->flags |= ENTITY_DEAD;
|
|
self->timer = 0.8f; /* damage cooldown */
|
|
|
|
/* Electric zap particles */
|
|
Vec2 zap_pos = vec2(
|
|
fb->pos.x + fb->size.x * 0.5f,
|
|
player->body.pos.y + player->body.size.y * 0.5f
|
|
);
|
|
SDL_Color zap_col = {136, 170, 255, 255};
|
|
particle_emit_spark(zap_pos, zap_col);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Emit shimmer particles */
|
|
if ((int)(fd->timer * 8.0f) % 2 == 0) {
|
|
float ry = self->body.pos.y + (float)(rand() % (int)self->body.size.y);
|
|
Vec2 origin = vec2(
|
|
self->body.pos.x + self->body.size.x * 0.5f,
|
|
ry
|
|
);
|
|
ParticleBurst burst = {
|
|
.origin = origin,
|
|
.count = 1,
|
|
.speed_min = 5.0f,
|
|
.speed_max = 15.0f,
|
|
.life_min = 0.1f,
|
|
.life_max = 0.25f,
|
|
.size_min = 0.5f,
|
|
.size_max = 1.5f,
|
|
.spread = 3.14159f,
|
|
.direction = 0.0f,
|
|
.drag = 0.5f,
|
|
.gravity_scale = 0.0f,
|
|
.color = {136, 170, 255, 200},
|
|
.color_vary = true,
|
|
};
|
|
particle_emit(&burst);
|
|
}
|
|
}
|
|
|
|
/* Damage cooldown */
|
|
if (self->timer > 0) {
|
|
self->timer -= dt;
|
|
}
|
|
}
|
|
|
|
static void force_field_render(Entity *self, const Camera *cam) {
|
|
(void)cam;
|
|
ForceFieldData *fd = (ForceFieldData *)self->data;
|
|
Body *body = &self->body;
|
|
|
|
if (g_spritesheet && self->anim.def) {
|
|
SDL_Rect src = animation_current_rect(&self->anim);
|
|
Sprite spr = {
|
|
.texture = g_spritesheet,
|
|
.src = src,
|
|
.pos = body->pos,
|
|
.size = vec2(SPRITE_CELL, SPRITE_CELL),
|
|
.flip_x = false,
|
|
.flip_y = false,
|
|
.layer = LAYER_ENTITIES,
|
|
.alpha = fd && fd->active ? 220 : 100,
|
|
};
|
|
renderer_submit(&spr);
|
|
} else {
|
|
SDL_Color color = fd && fd->active ?
|
|
(SDL_Color){100, 130, 255, 220} :
|
|
(SDL_Color){40, 50, 80, 100};
|
|
renderer_draw_rect(body->pos, body->size, color, LAYER_ENTITIES, cam);
|
|
}
|
|
}
|
|
|
|
static void force_field_destroy(Entity *self) {
|
|
free(self->data);
|
|
self->data = NULL;
|
|
}
|
|
|
|
void force_field_register(EntityManager *em) {
|
|
entity_register(em, ENT_FORCE_FIELD, force_field_update, force_field_render, force_field_destroy);
|
|
s_ffield_em = em;
|
|
}
|
|
|
|
Entity *force_field_spawn(EntityManager *em, Vec2 pos) {
|
|
Entity *e = entity_spawn(em, ENT_FORCE_FIELD, pos);
|
|
if (!e) return NULL;
|
|
|
|
e->body.size = vec2(FFIELD_WIDTH, FFIELD_HEIGHT);
|
|
e->body.gravity_scale = 0.0f;
|
|
e->health = 9999;
|
|
e->max_health = 9999;
|
|
e->flags |= ENTITY_INVINCIBLE;
|
|
e->damage = FFIELD_DAMAGE;
|
|
|
|
ForceFieldData *fd = calloc(1, sizeof(ForceFieldData));
|
|
fd->active = true;
|
|
fd->timer = FFIELD_ON_TIME;
|
|
e->data = fd;
|
|
e->timer = 0.0f; /* damage cooldown */
|
|
|
|
animation_set(&e->anim, &anim_force_field_on);
|
|
|
|
return e;
|
|
}
|