kitchen: README + MISSIONS/engine-c keep-drop notes

This commit is contained in:
Hernâni Marques 2026-09-17 21:48:48 +02:00
parent 1d351918da
commit 575d7d29b8
No known key found for this signature in database
130 changed files with 9817 additions and 1 deletions

585
engine-c/src/units.c Normal file
View file

@ -0,0 +1,585 @@
#include "kknd.h"
/* =========================================================================
* units.c - Unit lifecycle, movement, combat and the harvester economy.
* ========================================================================= */
static int unit_alloc_id(Game *g) { return g->unit_next_id++; }
Unit *unit_by_id(Game *g, int id) {
for (int i = 0; i < g->unit_count; i++)
if (!g->units[i].dead && g->units[i].id == id)
return &g->units[i];
return NULL;
}
int unit_spawn(Game *g, UnitType t, Faction f, float x, float y) {
if (g->unit_count >= MAX_UNITS) return -1;
const UnitDef *d = unit_def(t, f);
Unit *u = &g->units[g->unit_count++];
memset(u, 0, sizeof(Unit));
u->id = unit_alloc_id(g);
u->type = t;
u->faction = f;
u->x = x; u->y = y;
/* apply researched upgrades */
float dmg_m = 1.0f + 0.15f * (float)g->upg[f].lvl[UP_DAMAGE];
float arm_m = 1.0f + 0.15f * (float)g->upg[f].lvl[UP_ARMOR];
float spd_m = 1.0f + 0.10f * (float)g->upg[f].lvl[UP_SPEED];
float rel_m = 1.0f + 0.12f * (float)g->upg[f].lvl[UP_RELOAD];
u->hp = d->hp * arm_m; u->max_hp = d->hp * arm_m;
u->attack = d->attack * dmg_m; u->defense = d->defense;
u->speed = d->speed * spd_m; u->range = d->range; u->sight = d->sight;
u->reload_mul = rel_m;
u->heading = 0;
u->state = STATE_IDLE;
u->order = ORDER_NONE;
u->target_unit = -1; u->target_bld = -1;
u->atk_cd = 0;
u->cargo = 0;
u->selected = 0;
u->dead = 0;
u->anim = 0;
u->repath_timer = 0;
u->path.len = 0; u->path_idx = 0;
return g->unit_count - 1;
}
void unit_remove(Game *g, int idx) {
if (idx < 0 || idx >= g->unit_count) return;
if (g->units[idx].selected) {
for (int i = 0; i < g->sel_count; i++)
if (g->selection[i] == g->units[idx].id) {
g->selection[i] = g->selection[g->sel_count-1];
g->sel_count--; i--;
}
}
g->units[idx] = g->units[g->unit_count - 1];
g->unit_count--;
}
void unit_apply_damage(Game *g, Unit *u, float dmg) {
float eff = dmg - u->defense * 0.25f;
if (eff < 1) eff = 1;
u->hp -= eff;
particle_burst(g, u->x, u->y, 3, 2, 255, 200, 80);
if (u->hp <= 0 && !u->dead) {
u->dead = 1;
u->state = STATE_DEAD;
particle_burst(g, u->x, u->y, 14, 1, 255, 140, 40);
particle_burst(g, u->x, u->y, 8, 3, 90, 80, 70);
audio_sfx(2);
if (g->cam.shake < 7) g->cam.shake = 7;
if (u->faction == g->human)
game_log(g, "Unit lost: %s", unit_def(u->type, u->faction)->name);
/* leave a wreck decal on the battlefield */
if (g->wreck_count < MAX_WRECKS) {
Wreck *w = &g->wrecks[g->wreck_next];
w->x = u->x; w->y = u->y; w->type = u->type;
w->faction = u->faction; w->rot = u->heading;
g->wreck_next = (g->wreck_next + 1) % MAX_WRECKS;
if (g->wreck_count < MAX_WRECKS) g->wreck_count++;
}
}
}
static void unit_repath(Game *g, Unit *u, float gx, float gy) {
int sx, sy, gx_t, gy_t;
map_world_to_tile(u->x, u->y, &sx, &sy);
map_world_to_tile(gx, gy, &gx_t, &gy_t);
Path p;
if (path_find(&g->map, sx, sy, gx_t, gy_t, &p)) {
path_smooth(&g->map, &p);
u->path = p;
u->path_idx = (p.len > 1) ? 1 : 0;
} else {
u->path.len = 0;
}
u->repath_timer = 1.0f;
}
void unit_issue_move(Game *g, int idx, float x, float y, int attackmove) {
Unit *u = &g->units[idx];
u->order = attackmove ? ORDER_ATTACKMOVE : ORDER_MOVE;
u->dest_x = x; u->dest_y = y;
u->target_unit = -1; u->target_bld = -1;
u->state = STATE_MOVING;
unit_repath(g, u, x, y);
}
/* Spread a group of units into a grid formation centred on (cx,cy) so they
* don't all path to the same tile and pile up. */
void unit_issue_move_group(Game *g, const int *idxs, int n, float cx, float cy, int attackmove) {
if (n <= 0) return;
int cols = (int)(sqrtf((float)n) + 0.5f);
if (cols < 1) cols = 1;
float sp = 28.0f;
int halfc = (cols - 1) / 2;
for (int i = 0; i < n; i++) {
int c = i % cols, r = i / cols;
float ox = (c - halfc) * sp;
float oy = (r - halfc) * sp;
Unit *u = &g->units[idxs[i]];
if (u->type == UT_HARVESTER) {
unit_issue_move(g, idxs[i], cx, cy, attackmove);
continue;
}
u->order = attackmove ? ORDER_ATTACKMOVE : ORDER_MOVE;
u->dest_x = cx + ox; u->dest_y = cy + oy;
u->target_unit = -1; u->target_bld = -1;
u->state = STATE_MOVING;
unit_repath(g, u, u->dest_x, u->dest_y);
}
}
void unit_issue_attack_unit(Game *g, int idx, int target) {
Unit *u = &g->units[idx];
u->order = ORDER_ATTACK;
u->target_unit = target; u->target_bld = -1;
u->state = STATE_ATTACKING;
}
void unit_issue_attack_bld(Game *g, int idx, int target) {
Unit *u = &g->units[idx];
u->order = ORDER_ATTACK;
u->target_bld = target; u->target_unit = -1;
u->state = STATE_ATTACKING;
}
void unit_issue_gather(Game *g, int idx, float x, float y) {
Unit *u = &g->units[idx];
if (u->type != UT_HARVESTER) { unit_issue_move(g, idx, x, y, 0); return; }
u->order = ORDER_GATHER;
u->dest_x = x; u->dest_y = y;
u->state = STATE_MOVING;
unit_repath(g, u, x, y);
}
void unit_issue_gather_nearest(Game *g, int idx) {
Unit *u = &g->units[idx];
if (u->type != UT_HARVESTER) return;
float sx, sy;
if (map_find_nearest_scrap(&g->map, u->x, u->y, &sx, &sy)) {
u->order = ORDER_GATHER;
u->dest_x = sx; u->dest_y = sy;
u->state = STATE_MOVING;
unit_repath(g, u, sx, sy);
}
}
void unit_issue_return(Game *g, int idx) {
Unit *u = &g->units[idx];
if (u->type != UT_HARVESTER) return;
int rid = -1; float rbest = 1e18f;
for (int i = 0; i < g->bld_count; i++) {
Building *b = &g->buildings[i];
if (b->dead || b->faction != u->faction || b->type != BT_REFINERY) continue;
float bx = (float)(b->tx*TILE + b->w*TILE/2);
float by = (float)(b->ty*TILE + b->h*TILE/2);
float dd = kknd_dist2(u->x, u->y, bx, by);
if (dd < rbest) { rbest = dd; rid = b->id; }
}
if (rid < 0) return;
Building *b = bld_by_id(g, rid);
u->dest_x = (float)(b->tx*TILE + b->w*TILE/2);
u->dest_y = (float)(b->ty*TILE + b->h*TILE/2);
u->order = ORDER_RETURN;
u->state = STATE_MOVING;
unit_repath(g, u, u->dest_x, u->dest_y);
}
void unit_issue_stop(Game *g, int idx) {
Unit *u = &g->units[idx];
u->order = ORDER_STOP;
u->target_unit = -1; u->target_bld = -1;
u->state = STATE_IDLE;
u->hold = 0; u->attack_move = 0;
u->path.len = 0; u->path_idx = 0;
}
void unit_issue_hold(Game *g, int idx) {
Unit *u = &g->units[idx];
u->order = ORDER_STOP;
u->target_unit = -1; u->target_bld = -1;
u->state = STATE_IDLE;
u->hold = 1;
u->path.len = 0; u->path_idx = 0;
}
void unit_ability_use(Game *g, int idx) {
Unit *u = &g->units[idx];
if (u->ability == AB_SIEGE) {
u->ability_on = !u->ability_on;
audio_sfx(4);
} else if (u->ability == AB_HEAL) {
if (u->ability_cd <= 0) {
u->ability_cd = 8.0f;
float heal_r = 90.0f;
for (int k = 0; k < g->unit_count; k++) {
Unit *a = &g->units[k];
if (a->dead || a->faction != u->faction) continue;
if (kknd_dist2(u->x, u->y, a->x, a->y) < heal_r*heal_r)
a->hp = (a->hp + 120 > a->max_hp) ? a->max_hp : a->hp + 120;
}
particle_burst(g, u->x, u->y, 16, 1, 90, 255, 120);
audio_sfx(3);
}
}
}
void unit_select_single(Game *g, int idx, int add) {
if (!add) unit_clear_selection(g);
g->units[idx].selected = 1;
g->selection[g->sel_count++] = g->units[idx].id;
}
void unit_clear_selection(Game *g) {
for (int i = 0; i < g->unit_count; i++) g->units[i].selected = 0;
g->sel_count = 0;
}
void unit_select_all(Game *g, Faction f) {
unit_clear_selection(g);
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead || u->faction != f) continue;
u->selected = 1;
g->selection[g->sel_count++] = u->id;
}
}
void unit_select_in_rect(Game *g, int x0, int y0, int x1, int y1, int add) {
int minx = x0<x1?x0:x1, maxx = x0>x1?x0:x1;
int miny = y0<y1?y0:y1, maxy = y0>y1?y0:y1;
if (!add) unit_clear_selection(g);
int added = 0;
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead) continue;
int sx = (int)(u->x - g->cam.x);
int sy = (int)(u->y - g->cam.y);
if (sx >= minx && sx <= maxx && sy >= miny && sy <= maxy) {
if (u->faction == g->human && !u->selected) {
u->selected = 1;
g->selection[g->sel_count++] = u->id;
added++;
}
}
}
if (added) audio_sfx(4);
}
static int unit_find_enemy_unit(Game *g, Unit *u, float maxd) {
int best = -1; float bestd = maxd * maxd;
for (int i = 0; i < g->unit_count; i++) {
Unit *e = &g->units[i];
if (e->dead || e->faction == u->faction) continue;
float d = kknd_dist2(u->x, u->y, e->x, e->y);
if (d < bestd) { bestd = d; best = e->id; }
}
return best;
}
static int unit_find_enemy_bld(Game *g, Unit *u, float maxd) {
int best = -1; float bestd = maxd * maxd;
for (int i = 0; i < g->bld_count; i++) {
Building *b = &g->buildings[i];
if (b->dead || b->faction == u->faction) continue;
float bx = (float)(b->tx*TILE + b->w*TILE/2);
float by = (float)(b->ty*TILE + b->h*TILE/2);
float d = kknd_dist2(u->x, u->y, bx, by);
if (d < bestd) { bestd = d; best = b->id; }
}
return best;
}
static void unit_combat_step(Game *g, Unit *u, float dt) {
if (u->atk_cd > 0) u->atk_cd -= dt;
if (u->attack <= 0) return;
/* validate current target */
Unit *tu = (u->target_unit >= 0) ? unit_by_id(g, u->target_unit) : NULL;
Building *tb = NULL;
if (!tu && u->target_bld >= 0) {
for (int i = 0; i < g->bld_count; i++)
if (g->buildings[i].id == u->target_bld && !g->buildings[i].dead)
{ tb = &g->buildings[i]; break; }
}
float tx, ty;
if (tu) { tx = tu->x; ty = tu->y; }
else if (tb) { tx = (float)(tb->tx*TILE + tb->w*TILE/2);
ty = (float)(tb->ty*TILE + tb->h*TILE/2); }
else { tx = u->dest_x; ty = u->dest_y; }
float d = kknd_dist(u->x, u->y, tx, ty);
float rng = u->range * (u->ability_on ? 1.6f : 1.0f);
if ((tu || tb) && d <= rng) {
u->heading = (float)atan2(ty - u->y, tx - u->x);
if (u->atk_cd <= 0) {
int kind = (u->type == UT_ARTILLERY || u->type == UT_MISSILE || u->type == UT_TANK)
? 1 : 0;
if (tu) bullet_spawn(g, u->x, u->y, tu->id, -1, u->attack, u->faction, kind);
else bullet_spawn(g, u->x, u->y, -1, tb->id, u->attack, u->faction, kind);
u->atk_cd = ((u->type == UT_ARTILLERY) ? 2.2f
: (u->type == UT_INFANTRY ? 0.8f : 1.0f)) / u->reload_mul;
u->anim = 1.0f;
audio_sfx(1);
}
return;
}
/* need to close distance: keep moving along path toward target */
if (u->hold) return; /* hold position: fire but never chase */
if ((tu || tb) && u->order == ORDER_ATTACK) {
if (u->repath_timer <= 0 ||
kknd_dist2(u->dest_x, u->dest_y, tx, ty) > 64*64) {
u->dest_x = tx; u->dest_y = ty;
unit_repath(g, u, tx, ty);
}
u->state = STATE_MOVING;
}
}
static int harvester_find_refinery(Game *g, Unit *u) {
int rid = -1; float rbest = 1e18f;
for (int i = 0; i < g->bld_count; i++) {
Building *b = &g->buildings[i];
if (b->dead || b->faction != u->faction || b->type != BT_REFINERY) continue;
float bx = (float)(b->tx*TILE + b->w*TILE/2);
float by = (float)(b->ty*TILE + b->h*TILE/2);
float dd = kknd_dist2(u->x, u->y, bx, by);
if (dd < rbest) { rbest = dd; rid = b->id; }
}
return rid;
}
/* Harvester behaviour: travels in STATE_MOVING, loads only in
* STATE_GATHERING (whilst sitting on a scrap tile). */
static void unit_harvester_step(Game *g, Unit *u, float dt) {
(void)dt;
const UnitDef *d = unit_def(u->type, u->faction);
int tx, ty; map_world_to_tile(u->x, u->y, &tx, &ty);
Tile *t = map_tile(&g->map, tx, ty);
if (u->order == ORDER_NONE) u->order = ORDER_GATHER;
if (u->state == STATE_IDLE) {
if (t && t->terrain == T_SCRAP && t->scrap > 0 && u->cargo < d->cargo) {
u->state = STATE_GATHERING;
} else if (u->cargo >= d->cargo) {
int rid = harvester_find_refinery(g, u);
Building *b = (rid >= 0) ? bld_by_id(g, rid) : NULL;
if (b) {
u->dest_x = (float)(b->tx*TILE + b->w*TILE/2);
u->dest_y = (float)(b->ty*TILE + b->h*TILE/2);
u->state = STATE_MOVING; unit_repath(g, u, u->dest_x, u->dest_y);
}
} else {
float sx, sy;
if (map_find_nearest_scrap(&g->map, u->x, u->y, &sx, &sy)) {
u->order = ORDER_GATHER;
u->dest_x = sx; u->dest_y = sy;
u->state = STATE_MOVING; unit_repath(g, u, sx, sy);
}
}
return;
}
if (u->state == STATE_GATHERING) {
if (t && t->terrain == T_SCRAP && t->scrap > 0 && u->cargo < d->cargo) {
int take = ((int)t->scrap > 4) ? 4 : (int)t->scrap;
int add = (d->cargo - u->cargo > take) ? take : (int)(d->cargo - u->cargo);
u->cargo += add;
map_deplete_scrap(&g->map, tx, ty, add);
u->anim = 1.0f;
}
if (u->cargo >= d->cargo) {
int rid = harvester_find_refinery(g, u);
Building *b = (rid >= 0) ? bld_by_id(g, rid) : NULL;
if (b) {
u->dest_x = (float)(b->tx*TILE + b->w*TILE/2);
u->dest_y = (float)(b->ty*TILE + b->h*TILE/2);
u->state = STATE_MOVING; unit_repath(g, u, u->dest_x, u->dest_y);
} else u->state = STATE_IDLE;
} else if (!t || t->terrain != T_SCRAP || t->scrap == 0) {
float sx, sy;
if (map_find_nearest_scrap(&g->map, u->x, u->y, &sx, &sy)) {
u->dest_x = sx; u->dest_y = sy; u->state = STATE_MOVING;
unit_repath(g, u, sx, sy);
} else u->state = STATE_IDLE;
}
return;
}
/* travelling: handle arrival at refinery (deposit) or scrap (load) */
if (u->state == STATE_MOVING &&
(u->order == ORDER_GATHER || u->order == ORDER_RETURN)) {
int rid = harvester_find_refinery(g, u);
if (rid >= 0) {
Building *b = bld_by_id(g, rid);
float bx = (float)(b->tx*TILE + b->w*TILE/2);
float by = (float)(b->ty*TILE + b->h*TILE/2);
if (kknd_dist(u->x, u->y, bx, by) < TILE*1.4f) {
g->scrap[u->faction] += (int)u->cargo;
u->cargo = 0; audio_sfx(3);
float sx, sy;
if (map_find_nearest_scrap(&g->map, u->x, u->y, &sx, &sy)) {
u->dest_x = sx; u->dest_y = sy; u->state = STATE_MOVING;
unit_repath(g, u, sx, sy);
} else u->state = STATE_IDLE;
return;
}
}
int arrived = (u->path.len == 0 || u->path_idx >= u->path.len);
if (arrived) {
if (u->cargo < d->cargo && t && t->terrain == T_SCRAP && t->scrap > 0) {
u->state = STATE_GATHERING;
} else {
float sx, sy;
if (map_find_nearest_scrap(&g->map, u->x, u->y, &sx, &sy)) {
u->dest_x = sx; u->dest_y = sy; u->state = STATE_MOVING;
unit_repath(g, u, sx, sy);
} else u->state = STATE_IDLE;
}
}
}
}
void unit_update(Game *g, float dt) {
/* separation + obstacle-avoidance pass (cheap O(n^2) capped) */
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead || u->speed <= 0) continue;
float fx = 0, fy = 0;
float rad = (u->type == UT_TANK || u->type == UT_BEAST) ? 15.0f : 12.0f;
for (int j = 0; j < g->unit_count; j++) {
if (i == j) continue;
Unit *o = &g->units[j];
if (o->dead) continue;
float dx = u->x - o->x, dy = u->y - o->y;
float d2 = dx*dx + dy*dy;
float rr = rad * 2.0f;
if (d2 > 0 && d2 < rr*rr) {
float d = (float)sqrtf(d2);
float push = (rr - d) / rr; /* 0..1, stronger when closer */
fx += dx/d * push; fy += dy/d * push;
}
}
/* push out of building footprints */
for (int b = 0; b < g->bld_count; b++) {
Building *bd = &g->buildings[b];
if (bd->dead) continue;
float bx0 = bd->tx*TILE, by0 = bd->ty*TILE;
float bx1 = bx0 + bd->w*TILE, by1 = by0 + bd->h*TILE;
if (u->x > bx0-rad && u->x < bx1+rad && u->y > by0-rad && u->y < by1+rad) {
/* closest point on rect */
float cxp = u->x < bx0 ? bx0 : (u->x > bx1 ? bx1 : u->x);
float cyp = u->y < by0 ? by0 : (u->y > by1 ? by1 : u->y);
float dx = u->x - cxp, dy = u->y - cyp;
float d2 = dx*dx + dy*dy;
if (d2 < rad*rad) {
if (d2 < 0.01f) { dx = u->x - (bx0+bx1)/2; dy = u->y - (by0+by1)/2; d2 = dx*dx+dy*dy; }
float d = (float)sqrtf(d2);
fx += dx/d * 1.5f; fy += dy/d * 1.5f;
}
}
}
u->x += fx * 26 * dt;
u->y += fy * 26 * dt;
}
for (int i = 0; i < g->unit_count; i++) {
Unit *u = &g->units[i];
if (u->dead) continue;
if (u->anim > 0) u->anim -= dt * 3;
if (u->repath_timer > 0) u->repath_timer -= dt;
if (u->ability_cd > 0) u->ability_cd -= dt;
/* siege mode: immobile while active */
if (u->ability == AB_SIEGE && u->ability_on && u->state == STATE_MOVING) {
u->state = STATE_IDLE; u->path.len = 0; u->path_idx = 0;
}
/* medic / repair aura: heal nearby wounded allies */
if (u->ability == AB_HEAL) {
float heal_r = 70.0f;
float rate = (u->ability_cd > 0) ? 60.0f : 22.0f; /* burst pulses faster */
for (int k = 0; k < g->unit_count; k++) {
Unit *a = &g->units[k];
if (a->dead || a->faction != u->faction || a == u) continue;
if (a->hp >= a->max_hp) continue;
if (kknd_dist2(u->x, u->y, a->x, a->y) < heal_r*heal_r) {
a->hp = (a->hp + rate*dt > a->max_hp) ? a->max_hp : a->hp + rate*dt;
}
}
}
if (u->type == UT_HARVESTER) {
unit_harvester_step(g, u, dt);
}
if (u->attack > 0) {
if (u->order == ORDER_ATTACK) {
unit_combat_step(g, u, dt);
} else {
/* auto-acquire while idle / attack-moving */
if (u->state == STATE_IDLE || u->order == ORDER_ATTACKMOVE) {
float acq = u->hold ? u->range : u->sight;
int e = unit_find_enemy_unit(g, u, acq);
if (e < 0) {
int b = unit_find_enemy_bld(g, u, acq);
if (b >= 0) { u->target_bld = b; u->order = ORDER_ATTACK;
u->state = STATE_ATTACKING; }
} else {
u->target_unit = e; u->order = ORDER_ATTACK;
u->state = STATE_ATTACKING;
}
} else if (u->order == ORDER_ATTACKMOVE) {
if (u->atk_cd <= 0) {
int e = unit_find_enemy_unit(g, u, u->range + 8);
int b = unit_find_enemy_bld(g, u, u->range + 8);
if (e >= 0) { u->target_unit = e; unit_combat_step(g, u, dt); }
else if (b >= 0) { u->target_bld = b; unit_combat_step(g, u, dt); }
}
}
}
}
/* movement along path */
if (u->state == STATE_MOVING && u->path.len > 0) {
if (u->path_idx >= u->path.len) {
u->state = STATE_IDLE;
u->path.len = 0;
} else {
float wx, wy;
map_tile_to_world(u->path.nodes[u->path_idx].tx,
u->path.nodes[u->path_idx].ty, &wx, &wy);
float d = kknd_dist(u->x, u->y, wx, wy);
if (d < 4) {
u->path_idx++;
} else {
float sp = u->speed * dt;
u->heading = (float)atan2(wy - u->y, wx - u->x);
u->x += (wx - u->x) / d * sp;
u->y += (wy - u->y) / d * sp;
u->anim = 1.0f;
}
}
} else if (u->state == STATE_MOVING) {
/* no path: move straight */
float d = kknd_dist(u->x, u->y, u->dest_x, u->dest_y);
if (d < 4) { u->state = STATE_IDLE; }
else {
float sp = u->speed * dt;
u->x += (u->dest_x - u->x)/d * sp;
u->y += (u->dest_y - u->y)/d * sp;
}
}
}
/* cull dead */
for (int i = g->unit_count - 1; i >= 0; i--)
if (g->units[i].dead) unit_remove(g, i);
}