#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++; } /* Wheeled / tracked chassis: Unit.anim is continuous tyre phase (rad), not a pulse. */ static int unit_wheeled(UnitType t) { return t == UT_BUGGY || t == UT_JEEP || t == UT_HARVESTER || t == UT_BEAST || t == UT_MISSILE || t == UT_MEDIC; } static int unit_tracked(UnitType t) { return t == UT_TANK || t == UT_RAVAGER; } static int unit_rolling(UnitType t) { return unit_wheeled(t) || unit_tracked(t); } static void unit_roll_phase(Unit *u, float sp) { /* ~1 revolution per ~40 world px */ u->anim = fmodf(u->anim + (sp / 40.0f) * (float)(2.0 * M_PI), (float)(2.0 * M_PI)); if (u->anim < 0.0f) u->anim += (float)(2.0 * M_PI); } 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, 4, 2, 255, 200, 80); if (u->hp <= 0 && !u->dead) { u->dead = 1; u->state = STATE_DEAD; /* denser KKND2-like death: fire core + debris ring + smoke + scorch */ particle_burst(g, u->x, u->y, 22, 1, 255, 140, 40); particle_burst(g, u->x, u->y, 14, 3, 90, 80, 70); particle_burst(g, u->x + 6, u->y - 4, 10, 0, 110, 100, 90); particle_burst(g, u->x - 5, u->y + 5, 8, 2, 255, 220, 90); particle_burst(g, u->x + 3, u->y + 7, 8, 1, 60, 50, 45); /* scorch soot */ particle_burst(g, u->x - 4, u->y - 6, 6, 0, 70, 55, 40); 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); /* lasting terrain scar under corpse (RUBBLE/SCRAP, soft/walkable) */ map_scar_unit_death(&g->map, u->x, u->y, u->type); /* 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 = x0x1?x0:x1; int miny = y0y1?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; /* muzzle offset along heading so shot/live FX sit at barrel tip */ float mx = u->x + cosf(u->heading) * 14.0f; float my = u->y + sinf(u->heading) * 14.0f; if (tu) bullet_spawn(g, mx, my, tu->id, -1, u->attack, u->faction, kind); else bullet_spawn(g, mx, my, -1, tb->id, u->attack, u->faction, kind); particle_burst(g, mx, my, 5, 2, 255, 230, 120); particle_burst(g, mx, my, 3, 1, 255, 160, 50); u->atk_cd = ((u->type == UT_ARTILLERY) ? 2.2f : (u->type == UT_INFANTRY ? 0.8f : 1.0f)) / u->reload_mul; /* rolling chassis keep continuous tyre phase — no fire pulse */ if (!unit_rolling(u->type)) 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); if (!unit_rolling(u->type)) 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; /* infantry/pulse anim decays; wheeled/tracked keep phase radians */ if (!unit_rolling(u->type) && 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; if (unit_rolling(u->type)) unit_roll_phase(u, sp); else 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->heading = (float)atan2(u->dest_y - u->y, u->dest_x - u->x); u->x += (u->dest_x - u->x)/d * sp; u->y += (u->dest_y - u->y)/d * sp; if (unit_rolling(u->type)) unit_roll_phase(u, sp); else u->anim = 1.0f; } } } /* cull dead */ for (int i = g->unit_count - 1; i >= 0; i--) if (g->units[i].dead) unit_remove(g, i); }