commit 8df6b05358ace1afad3c0a43315c4b15c430c4a8
Author: rani <clagv.randomgames@gmail.com>
Date: Tue Jun 03 21:04:14 2025 +0000
diff --git a/td/README.md b/td/README.md
new file mode 100644
index 0000000..4cf4838
--- /dev/null
+++ b/td/README.md
@@ -00 +134 @@
+# TD
+
+A simple, randomly generated, tower defense game.
+
+Click on a turret's name in the shop to begin purchasing, then click on a tile
+to place it there. Press q to abort the purchase. Click on a turret (that has
+already been placed) to open the upgrade menu. Click on the info button to see
+the turret's stats. Buttons (in any menu) can be clicked on if they are
+highlighted. Otherwise, they cannot be clicked on. Space to pause, any to
+resume. q to quit.
+
+Start by purchasing a turret and placing it on the map such that it is in range
+of a path tile. Spikes may only be placed on path tiles and will run out after a
+certain number of enemies run over them. When an enemy is killed, cash is
+rewarded and the score increases. Once enough cash is earned, a turret may be
+upgraded from its upgrade menu. Otherwise, another turret can be purchased and
+placed on the map. Certain turrets may deal splash damage to enemies so that one
+attack will damage multiple enemies at once. After killing enough enemies, the
+round is progressed and a new wave of enemies is spawned. As the rounds
+progress, enemies will gain more health and move faster.
+
+# Config
+
+* `X`: Board width (integer)
+* `Y`: Board height (integer)
+* `SEED`: PRNG seed (integer)
+* `DELAY`: The delay (in milliseconds) between game ticks (integer)
+* `PATH_BENDS`: The number of bends in the path (integer)
+* `ATTACK_ANIMATION_DELAY`: The delay (ms) to animate attacks (integer)
+* `MAX_ENEMIES`: The maximum number of enemies in a round (integer)
+* `MAX_TURRETS`: The maximum number of turrets allowed on the map (integer)
+* `STARTING_CASH`: Amount of cash at the start of the game (integer)
+* `STARTING_LIVES`: Amount of lives at the start of the game (integer)
+* `STARTING_ROUND`: The round at which the game starts (integer)
diff --git a/td/td.c b/td/td.c
new file mode 100644
index 0000000..d770976
--- /dev/null
+++ b/td/td.c
@@ -00 +1965 @@
+#include <ncurses.h>
+#include <stdbool.h>
+#include <string.h>
+#include <stdlib.h>
+
+/* BEGIN CONFIG */
+#ifndef X
+#define X 64
+#endif /* X */
+
+#ifndef Y
+#define Y 32
+#endif /* Y */
+
+#ifndef SEED
+#include <time.h>
+#define SEED time(NULL)
+#endif /* SEED */
+
+#ifndef DELAY
+#define DELAY 100
+#endif /* DELAY */
+
+#ifndef PATH_BENDS
+#define PATH_BENDS 3
+#endif /* PATH_BENDS */
+
+#ifndef ATTACK_ANIMATION_DELAY
+#define ATTACK_ANIMATION_DELAY 35
+#endif /* ATTACK_ANIMATION_DELAY */
+
+#ifndef MAX_ENEMIES
+#define MAX_ENEMIES 256
+#endif /* MAX_ENEMIES */
+
+#ifndef MAX_TURRETS
+#define MAX_TURRETS 128
+#endif /* MAX_TURRETS */
+
+#ifndef STARTING_CASH
+#define STARTING_CASH 120
+#endif /* STARTING_CASH */
+
+#ifndef STARTING_LIVES
+#define STARTING_LIVES 25
+#endif /* STARTING_LIVES */
+
+#ifndef STARTING_ROUND
+#define STARTING_ROUND 1
+#endif /* STARTING_ROUND */
+/* END CONFIG */
+
+#define ARRLEN(a) (sizeof(a)/sizeof(*a))
+
+#define BIN1(a) (a & 1)
+#define BIN2(a,b) ((BIN1(a) << 1 ) | BIN1(b))
+#define BIN4(a,b,c,d) ((BIN2(a,b) << 2) | BIN2(c,d))
+#define BIN8(a,b,c,d,e,f,g,h) ((BIN4(a,b,c,d) << 4) | BIN4(e,f,g,h))
+
+#define CELL_PATH_MASK BIN8(0,0,0,0,1,1,0,0)
+#define INT_TO_CELL_PATH(i) (((i) & (CELL_PATH_MASK >> 2)) << 2)
+#define CELL_PATH_TO_INT(c) (((c) & CELL_PATH_MASK) >> 2)
+
+#define CELL_TURRET_MASK BIN8(1,1,1,1,0,0,0,0)
+#define INT_TO_CELL_TURRET(i) (((i) & (CELL_TURRET_MASK >> 4)) << 4)
+#define CELL_TURRET_TO_INT(c) (((c) & CELL_TURRET_MASK) >> 4)
+
+#define SHOP_STARTX (X + 2)
+#define SHOP_STARTY (1)
+
+#define SHOP_ID_TO_X(id) (SHOP_STARTX)
+#define SHOP_ID_TO_Y(id) ((id) * 2 + SHOP_STARTY + 2)
+
+#define Y_TO_SHOP_ID(y) (((y) - SHOP_STARTY) % 2 == 1 ? \
+ (-1) : \
+ (((y) - SHOP_STARTY - 2) / 2))
+
+
+enum color {
+ RED = 1,
+ GREEN,
+ YELLOW,
+ BLUE,
+ MAGENTA,
+ CYAN,
+};
+
+enum state {
+ RUNNING,
+ PAUSED,
+ GAME_OVER,
+ QUIT,
+};
+
+enum cell {
+ CELL_EMPTY = 0,
+ CELL_PATH = 1 << 0,
+ CELL_TURRET = 1 << 1,
+};
+
+enum cell_path {
+ CELL_PATH_UP,
+ CELL_PATH_DOWN,
+ CELL_PATH_LEFT,
+ CELL_PATH_RIGHT,
+};
+
+
+struct enemies {
+ struct enemy {
+ int x;
+ int y;
+ int count;
+ int ticks;
+ } enemies[MAX_ENEMIES];
+
+ int idx;
+ int last_round;
+ int spawned;
+ int killed;
+ int to_spawn;
+
+ int spawnx;
+ int spawny;
+};
+
+
+struct {
+ char * name;
+ char symbol;
+ int cost; // cost to purchase
+ int radius; // attack radius
+ int rsplash; // slash damage radius
+ int damage; // damage per attack
+ int dsplash; // splash damage
+ int stack; // amount of turrets per tile (-1 for attacking turrets)
+ int ticks; // ticks between attacks
+ int n_upgrades; // number of upgrades
+ struct {
+ int cost;
+ int radius;
+ int rsplash;
+ int damage;
+ int dsplash;
+ int ticks;
+ } upgrades[4];
+} turrets[] = {
+/* Name Sym Cost Rad RSplsh Dmg DSplsh Stack Tick nUpgr*/
+ {"Spikes", 'M', 25, 0, 0, 1, 0, 10, 1, 0,},
+ {"Gunner", '%', 100, 3, 0, 1, 0, -1, 5, 3, {
+ { 25, 4, 0, 1, 0, 5 },
+ { 50, 4, 0, 2, 0, 5 },
+ { 75, 4, 0, 3, 0, 4 },
+ }},
+ {"Bomb Lobber", '&', 165, 3, 1, 1, 1, -1, 10, 3, {
+ { 50, 3, 2, 1, 1, 10 },
+ {100, 4, 2, 1, 2, 10 },
+ {150, 4, 2, 1, 2, 6 },
+ }},
+ {"Machine Gunner", '$', 400, 2, 0, 1, 0, -1, 3, 4, {
+ {200, 3, 0, 1, 0, 3 },
+ {400, 4, 0, 2, 0, 2 },
+ {800, 5, 0, 4, 0, 2 },
+ {8000, 5, 0, 4, 0, 1 },
+ }},
+};
+
+struct turrets {
+ struct spawned_turret {
+ int x;
+ int y;
+ int id;
+ int radius;
+ int rsplash;
+ int damage;
+ int dsplash;
+ int stack;
+ int ticks;
+ int level; // upgrade level
+ int kills;
+ } spawned[MAX_TURRETS];
+
+ int idx;
+} spawned_turrets;
+
+
+unsigned long ticks = 0;
+
+
+void enemies_push(struct enemies * enemies, int x, int y, int count, int ticks) {
+ int idx = enemies->idx;
+ enemies->enemies[idx].x = x;
+ enemies->enemies[idx].y = y;
+ enemies->enemies[idx].count = count;
+ enemies->enemies[idx].ticks = ticks;
+ enemies->spawned += count;
+ enemies->idx++;
+}
+
+
+void enemies_pop(struct enemies * enemies, int i) {
+ enemies->idx--;
+ memmove(&enemies->enemies[i], &enemies->enemies[i + 1],
+ (MAX_ENEMIES - (i + 1)) * sizeof(*enemies->enemies));
+}
+
+
+void turrets_push(
+ struct turrets * spawned_turrets, char grid[X][Y], int x, int y, int id
+) {
+ int idx = spawned_turrets->idx;
+ spawned_turrets->spawned[idx].x = x;
+ spawned_turrets->spawned[idx].y = y;
+ spawned_turrets->spawned[idx].id = id;
+ spawned_turrets->spawned[idx].radius = turrets[id].radius;
+ spawned_turrets->spawned[idx].rsplash = turrets[id].rsplash;
+ spawned_turrets->spawned[idx].damage = turrets[id].damage;
+ spawned_turrets->spawned[idx].dsplash = turrets[id].dsplash;
+ spawned_turrets->spawned[idx].stack = turrets[id].stack;
+ spawned_turrets->spawned[idx].ticks = turrets[id].ticks;
+ spawned_turrets->spawned[idx].level = 0;
+ spawned_turrets->spawned[idx].kills = 0;
+ spawned_turrets->idx++;
+
+ grid[x][y] |= CELL_TURRET | INT_TO_CELL_TURRET(id);
+}
+
+void turrets_pop(struct turrets * spawned, char grid[X][Y], int x, int y, int i) {
+ spawned->idx--;
+ memmove(&spawned->spawned[i], &spawned->spawned[i + 1],
+ (MAX_TURRETS - (i + 1)) * sizeof(*spawned->spawned));
+
+ grid[x][y] &= ~CELL_TURRET | INT_TO_CELL_TURRET(0xFF);
+}
+
+
+int rand_range(int lo, int hi) {
+ return rand() % (hi - lo) + lo;
+}
+
+
+void generate_path(char grid[X][Y], struct enemies * enemies) {
+ int spawnx = 0;
+ int spawny = rand() % Y;
+ enemies->spawnx = spawnx;
+ enemies->spawny = spawny;
+
+ int lastx = spawnx;
+ int lasty = spawny;
+ for (int i = 0; i < PATH_BENDS; i++) {
+ int bendx = rand_range(lastx + 1, (i + 1) * X / PATH_BENDS);
+ int bendy = rand_range(0, Y);
+
+ // horizontal run
+ for (int x = lastx; x < bendx; x++) {
+ grid[x][lasty] |= CELL_PATH | INT_TO_CELL_PATH(CELL_PATH_RIGHT);
+ }
+
+ // vertical connection
+ if (lasty != bendy) {
+ if (lasty < bendy) for (int y = lasty; y < bendy; y++) {
+ grid[bendx][y] |= CELL_PATH | INT_TO_CELL_PATH(CELL_PATH_DOWN);
+ } else for (int y = lasty; y > bendy; y--) {
+ grid[bendx][y] |= CELL_PATH | INT_TO_CELL_PATH(CELL_PATH_UP);
+ }
+ }
+
+ lastx = bendx;
+ lasty = bendy;
+ }
+
+ // connect to edge
+ for (int x = lastx; x < X; x++) {
+ grid[x][lasty] |= CELL_PATH | INT_TO_CELL_PATH(CELL_PATH_RIGHT);
+ }
+}
+
+
+char grid_getc(char grid[X][Y], int x, int y) {
+ char c = grid[x][y];
+ char out = ' ';
+ if (c & CELL_TURRET) out = turrets[CELL_TURRET_TO_INT(c)].symbol;
+ else if (c & CELL_PATH) switch(CELL_PATH_TO_INT(c)) {
+ case CELL_PATH_UP: out = '^'; break;
+ case CELL_PATH_DOWN: out = 'v'; break;
+ case CELL_PATH_LEFT: out = '<'; break;
+ case CELL_PATH_RIGHT: out = '>'; break;
+ }
+ return out;
+}
+
+
+int grid_getcolor(char grid[X][Y], int x, int y) {
+ char c = grid[x][y];
+ int out = 0;
+ if (c & CELL_TURRET) out = A_UNDERLINE;
+ else if (c & CELL_PATH) out = A_DIM;
+ return out;
+}
+
+
+void draw_grid(char grid[X][Y]) {
+ addch('+');
+ for (int x = 0; x < X; x++) addch('-');
+ addch('+');
+ addch('\n');
+ for (int y = 0; y < Y; y++) {
+ addch('|');
+ for (int x = 0; x < X; x++) {
+ int cp = grid_getcolor(grid, x, y);
+ attron(cp);
+ addch(grid_getc(grid, x, y));
+ attroff(cp);
+ }
+ addstr("|\n");
+ }
+ addch('+');
+ for (int x = 0; x < X; x++) addch('-');
+ addch('+');
+ addch('\n');
+}
+
+
+
+int get_shop_str(char * s, int n, int i) {
+ return snprintf(s, n, "%s: $%d", turrets[i].name,
+ turrets[i].cost);
+}
+
+
+int yx_to_shop_id(int y, int x) {
+ int id = Y_TO_SHOP_ID(y);
+ if (id < 0) return -1;
+ if (id >= ARRLEN(turrets)) return -1;
+
+ int len = get_shop_str(NULL, 0, id);
+ int off = x - SHOP_ID_TO_X(id);
+ if (off < 0 || off >= len) return -1;
+ return id;
+}
+
+
+void draw_shop(int cash) {
+ int y = SHOP_STARTY;
+ int x = SHOP_STARTX;
+ mvaddstr(y, x, "Shop:");
+ mvaddstr(++y, x, "-----");
+
+ char s[25];
+ for (int i = 0; i < ARRLEN(turrets); i++) {
+ x = SHOP_ID_TO_X(i);
+ y = SHOP_ID_TO_Y(i);
+ move(y, x);
+ int len = get_shop_str(s, ARRLEN(s), i);
+ if (cash >= turrets[i].cost) attron(A_REVERSE);
+ addstr(s);
+ if (cash >= turrets[i].cost) attroff(A_REVERSE);
+ }
+}
+
+
+void erase_shop(void) {
+ int y = SHOP_STARTY;
+ int x = SHOP_STARTX;
+ move(y, x);
+ clrtoeol();
+ move(++y, x);
+ clrtoeol();
+
+ for (int i = 0; i < ARRLEN(turrets); i++) {
+ x = SHOP_ID_TO_X(i);
+ y = SHOP_ID_TO_Y(i);
+ move(y, x);
+ clrtoeol();
+ }
+}
+
+
+void draw_radius(char grid[X][Y], int x, int y, int r) {
+ attron(A_REVERSE);
+ for (int rx = -r + 1; rx < r; rx++) {
+ for (int ry = -r + 1; ry < r; ry++) {
+ if (rx*rx + ry*ry > r*r) continue;
+ int dx = rx + x;
+ int dy = ry + y;
+ if (dx < 0) dx = 0;
+ else if (dx >= X) dx = X - 1;
+ if (dy < 0) dy = 0;
+ else if (dy >= Y) dy = Y - 1;
+ move(dy + 1, dx + 1);
+ addch(grid_getc(grid, dx, dy));
+ }
+ }
+ attroff(A_REVERSE);
+}
+
+
+int try_purchase(char grid[X][Y], int id, int cash, struct turrets * spawned_turrets) {
+ int cost = turrets[id].cost;
+ int radius = turrets[id].radius;
+ if (cost > cash) return -1;
+ timeout(-1);
+
+ bool done = false;
+ while (!done) {
+ move(Y + 2, X + 2 - 10);
+ addstr("q to abort");
+ move(Y + 3, X + 2 - 14);
+ addstr("Click to place");
+ refresh();
+
+ switch (getch()) {
+ case 'q':
+ cost = 0;
+ done = true;
+ break;
+ case KEY_MOUSE:;
+ MEVENT e;
+ if (getmouse(&e) != OK) break;
+ int x = e.x - 1;
+ int y = e.y - 1;
+ if (x < 0 || x >= X || y < 0 || y >= Y) break;
+ // turrets with a nonnegative stack must be placed on paths
+ // other turrets can only by placed on empty cells
+ if ((grid[x][y] & CELL_TURRET) ||
+ (turrets[id].stack < 0 && (grid[x][y] & CELL_PATH))) {
+ break;
+ }
+ if (turrets[id].stack > 0 && !(grid[x][y] & CELL_PATH)) break;
+
+ // draw radius
+ draw_radius(grid, x, y, radius);
+ move(e.y, e.x);
+ addch(turrets[id].symbol);
+ move(Y + 3, X + 2 - 15);
+ addstr("Enter to accept");
+ refresh();
+ if (getch() != '\n') {
+ cost = 0;
+ done = true;
+ break;
+ }
+
+ turrets_push(spawned_turrets, grid, x, y, id);
+ done = true;
+ break;
+ }
+ }
+
+ timeout(DELAY);
+ return cost;
+}
+
+
+int try_upgrade(int id, int cash, struct turrets * spawned_turrets, char grid[X][Y]) {
+ int cost = 0;
+ timeout(-1);
+
+ struct spawned_turret * st = &spawned_turrets->spawned[id];
+
+ erase_shop();
+ int y = SHOP_STARTY;
+ int x = SHOP_STARTX;
+ move(y, x);
+ addstr(turrets[st->id].name);
+ addch(':');
+ move(++y, x);
+ int nlen = strlen(turrets[st->id].name);
+ for (int i = 0; i < nlen + 1; i++) addch('-');
+
+ move(SHOP_ID_TO_Y(0), SHOP_ID_TO_X(0));
+ attron(A_REVERSE);
+ addstr("Turret Info");
+ attroff(A_REVERSE);
+
+ int tid = st->id;
+ int max_level = turrets[tid].n_upgrades;
+ int cur_level = st->level;
+ int up_idx = cur_level;
+ // sell for 75% of purchasing cost
+ int selling = 0;
+ selling += turrets[tid].cost;
+ for (int i = 0; i < cur_level; i++) {
+ selling += turrets[tid].upgrades[i].cost;
+ }
+ if (turrets[tid].stack > 0) {
+ selling *= st->stack;
+ selling /= turrets[tid].stack;
+ }
+ selling *= 3;
+ selling /= 4;
+ move(SHOP_ID_TO_Y(1), SHOP_ID_TO_X(1));
+ attron(A_REVERSE);
+ printw("Sell: -$%d", selling);
+ attroff(A_REVERSE);
+
+ if (cur_level < max_level) {
+ cost = turrets[tid].upgrades[up_idx].cost;
+ bool affordable = true;
+ if (cash < cost) affordable = false;
+ move(SHOP_ID_TO_Y(2), SHOP_ID_TO_X(2));
+ if (affordable) attron(A_REVERSE);
+ printw("Upgrade: $%d", cost);
+ if (affordable) attroff(A_REVERSE);
+
+
+ }
+
+ draw_radius(grid, st->x, st->y, st->radius);
+
+ bool done = false;
+ while (!done) {
+ move(Y + 2, X + 2 - 12);
+ addstr("Any to abort");
+ // clear the pause/resume prompt
+ move(Y + 3, X + 2 - 14);
+ clrtoeol();
+ refresh();
+
+ switch (getch()) {
+ case KEY_MOUSE:;
+ MEVENT e;
+ if (getmouse(&e) != OK) break;
+ int x = e.x - 1;
+ int y = e.y - 1;
+ int sid = yx_to_shop_id(e.y, e.x);
+ // turret info
+ if (sid == 0) {
+ int kills = st->kills;
+ int ticks = st->ticks;
+ int rad = st->radius;
+ int dmg = st->damage;
+ int rsp = st->rsplash;
+ int dsp = st->dsplash;
+ int stk = st->stack;
+ int py, px;
+ py = SHOP_STARTY;
+ px = SHOP_STARTX;
+ erase_shop();
+ move(py, px);
+ addstr(turrets[st->id].name);
+ addch(':');
+ move(++py, px);
+ for (int i = 0; i < nlen + 1; i++) addch('-');
+
+ mvprintw(++py, px, "Kills: %d", kills);
+ mvprintw(++py, px, "Level: %d", cur_level);
+ mvprintw(++py, px, "Attack Ticks: %d", ticks);
+ mvprintw(++py, px, "Radius: %d", rad);
+ mvprintw(++py, px, "Damage: %d", dmg);
+ if (rsp > 0 && dsp > 0) {
+ mvprintw(++py, px, "Splash Radius: %d", rsp);
+ mvprintw(++py, px, "Splash Damage: %d", dsp);
+ }
+ if (stk > 0) {
+ mvprintw(++py, px, "Stacked: %d", stk);
+ }
+
+ move(Y + 2, X + 2 - 13);
+ addstr("Any to finish");
+ refresh();
+ getch();
+ done = true;
+ cost = 0;
+ break;
+ // sell turret
+ } else if (sid == 1) {
+ cost = -selling;
+ done = true;
+ // upgrade turret
+ } else if (sid == 2) {
+ if (cash < cost) break;
+ st->level++;
+ st->radius = turrets[tid].upgrades[up_idx].radius;
+ st->rsplash = turrets[tid].upgrades[up_idx].rsplash;
+ st->damage = turrets[tid].upgrades[up_idx].damage;
+ st->dsplash = turrets[tid].upgrades[up_idx].dsplash;
+ st->ticks = turrets[tid].upgrades[up_idx].ticks;
+ done = true;
+ break;
+ }
+ break;
+ default:
+ cost = 0;
+ done = true;
+ break;
+ }
+ }
+
+ timeout(DELAY);
+ return cost;
+}
+
+
+void draw_enemies(struct enemies * enemies) {
+ for (int i = 0; i < enemies->idx; i++) {
+ if (enemies->enemies[i].count == 0) continue;
+ int cp = 0;
+ switch (enemies->enemies[i].count) {
+ case 1: cp = COLOR_PAIR(RED); break;
+ case 2: cp = COLOR_PAIR(CYAN); break;
+ case 3: cp = COLOR_PAIR(GREEN); break;
+ case 4: cp = COLOR_PAIR(YELLOW); break;
+ default: cp = COLOR_PAIR(MAGENTA); break;
+ }
+ int x = enemies->enemies[i].x;
+ int y = enemies->enemies[i].y;
+ move(1 + y, 1 + x);
+ attron(cp);
+ addch('@');
+ attroff(cp);
+ }
+}
+
+
+int get_spawn_rate(int round) {
+ if (round <= 10) return 5;
+ if (round <= 35) return rand_range(2,4);
+ return rand_range(1, 4);
+}
+
+
+int get_speed(int round) {
+ if (round <= 10) return 5;
+ if (round <= 35) return rand_range(3, 5);
+ return rand_range(1, 3);
+}
+
+
+int get_stack(int round) {
+ int lower_rounds[] = {
+ 1, 1, 1, 2, 2,
+ };
+ int mid_rounds[] = {
+ 1, 1, 1, 1, 1, 1, 1, 1, 1,
+ 2, 2, 2, 2, 2, 2, 2, 2, 2,
+ 2, 2, 2, 2, 2, 3, 3, 3, 3,
+ 3, 3, 3, 3, 3, 3, 4, 4, 4,
+ };
+ int mid_rounds2[] = {
+ 2, 2, 2, 3, 3, 3, 3, 3, 3,
+ 4, 4, 4, 4, 4, 4, 4, 5, 5,
+ 5, 5, 6, 6, 6, 7, 7, 8, 9
+ };
+ if (round <= 10) return lower_rounds[rand_range(0, ARRLEN(lower_rounds))];
+ if (round <= 35) return mid_rounds[rand_range(0, ARRLEN(mid_rounds))];
+ if (round <= 60) return mid_rounds2[rand_range(0, ARRLEN(mid_rounds2))];
+ return rand_range(5, 20);
+}
+
+
+int get_to_spawn(int round) {
+ int ret = 0;
+ if (round <= 3) ret = (round + 1) * 4 + rand_range(0, 2);
+ else if (round <= 20) ret = round * 3 + rand_range(0, 5);
+ else if (round <= 40) ret = 2 * MAX_ENEMIES / 3 + rand_range(-5, 5);
+ else ret = 7 * MAX_ENEMIES / 8 + rand_range(-5, 5);
+ return ret > MAX_ENEMIES - 20 ? MAX_ENEMIES - 20 : ret;
+}
+
+
+int spawn_enemies(struct enemies * enemies, char grid[X][Y], int round) {
+ if (enemies->last_round != round) {
+ enemies->spawned = 0;
+ enemies->killed = 0;
+ enemies->last_round = round;
+ enemies->to_spawn = get_to_spawn(round);
+ }
+
+ int deaths = 0;
+
+ // advance all enemies
+ // iterate backward so that popping doesn't mess up the iteration
+ for (int i = enemies->idx - 1; i >= 0; i--) {
+ if (enemies->enemies[i].count == 0) continue;
+ if (ticks % enemies->enemies[i].ticks != 0) continue;
+ int x = enemies->enemies[i].x;
+ int y = enemies->enemies[i].y;
+ int c = grid[x][y];
+ if (c & CELL_PATH) switch (CELL_PATH_TO_INT(c)) {
+ case CELL_PATH_UP: y--; break;
+ case CELL_PATH_DOWN: y++; break;
+ case CELL_PATH_LEFT: x--; break;
+ case CELL_PATH_RIGHT: x++; break;
+ }
+ enemies->enemies[i].x = x;
+ enemies->enemies[i].y = y;
+ if (x >= X || y >= Y) {
+ enemies->killed += enemies->enemies[i].count;
+ deaths += enemies->enemies[i].count;
+ enemies_pop(enemies, i);
+ }
+ }
+
+ if (enemies->spawned >= enemies->to_spawn) return deaths;
+
+ int spawn_rate = get_spawn_rate(round);
+ int speed = get_speed(round);
+ int stack = get_stack(round);
+ if (ticks % spawn_rate == 0) {
+ enemies_push(enemies, enemies->spawnx, enemies->spawny, stack, speed);
+ }
+
+ return deaths;
+}
+
+
+int find_nearest_enemy(struct enemies * enemies, int x, int y, int rad) {
+ if (enemies->idx == 0) return -1;
+
+ int nearestx = X + 1;
+ int nearesty = Y + 1;
+ int nearestid = -1;
+
+ for (int i = 0; i < enemies->idx; i++) {
+ int ex = enemies->enemies[i].x - x;
+ int ey = enemies->enemies[i].y - y;
+ if (ex*ex + ey*ey > rad*rad) continue;
+ if (ex*ex + ey*ey < nearestx*nearestx + nearesty*nearesty) {
+ nearestx = ex;
+ nearesty = ey;
+ nearestid = i;
+ }
+ }
+
+ return nearestid;
+}
+
+
+int attack_enemy(struct enemies * enemies, int id, int dmg) {
+ int kills = enemies->enemies[id].count;
+ enemies->enemies[id].count -= dmg;
+ if (dmg >= kills) {
+ enemies_pop(enemies, id);
+ } else kills = dmg;
+ enemies->killed += kills;
+ return kills;
+}
+
+
+int splash_enemies(struct enemies * enemies, int x, int y, int rad, int dmg) {
+ int kills = 0;
+ for (int i = 0; i < enemies->idx; i++) {
+ int ex = enemies->enemies[i].x - x;
+ int ey = enemies->enemies[i].y - y;
+ if (ex*ex + ey*ey > rad*rad) continue;
+
+ kills += attack_enemy(enemies, i, dmg);
+ }
+
+ return kills;
+}
+
+
+int run_turrets(struct turrets * spawned, struct enemies * enemies, char grid[X][Y]) {
+ int kills = 0;
+
+ for (int i = 0; i < spawned->idx; i++) {
+ int tid = spawned->spawned[i].id;
+ int tx = spawned->spawned[i].x;
+ int ty = spawned->spawned[i].y;
+ int radius = spawned->spawned[i].radius;
+ int damage = spawned->spawned[i].damage;
+ int rsplash = spawned->spawned[i].rsplash;
+ int dsplash = spawned->spawned[i].dsplash;
+
+ if (ticks % spawned->spawned[i].ticks != 0) continue;
+
+ int nearest = find_nearest_enemy(enemies, tx, ty, radius);
+ if (nearest < 0) continue;
+
+ int nx = enemies->enemies[nearest].x;
+ int ny = enemies->enemies[nearest].y;
+
+ // damage animation
+ move(ny + 1, nx + 1);
+ attron(A_REVERSE);
+ addch(grid_getc(grid, nx, ny));
+ refresh();
+ napms(ATTACK_ANIMATION_DELAY);
+ attroff(A_REVERSE);
+ refresh();
+
+ int just_killed = attack_enemy(enemies, nearest, damage);
+ if (rsplash > 0 && dsplash > 0)
+ just_killed += splash_enemies(enemies, nx, ny, rsplash, dsplash);
+
+ kills += just_killed;
+ if (spawned->spawned[i].stack > 0) {
+ spawned->spawned[i].stack -= just_killed;
+ if (spawned->spawned[i].stack <= 0) {
+ turrets_pop(spawned, grid, tx, ty, i);
+ }
+ }
+
+ spawned->spawned[i].kills += just_killed;
+ }
+
+ return kills;
+}
+
+
+bool no_enemies(struct enemies * enemies) {
+ return enemies->killed >= enemies->to_spawn;
+}
+
+
+int main(void) {
+ srand(SEED);
+
+ initscr();
+ noecho();
+ curs_set(0);
+ keypad(stdscr, TRUE);
+ timeout(DELAY);
+
+ mousemask(BUTTON1_CLICKED, NULL);
+
+ use_default_colors();
+ start_color();
+ init_pair(RED, COLOR_RED, -1);
+ init_pair(GREEN, COLOR_GREEN, -1);
+ init_pair(YELLOW, COLOR_YELLOW, -1);
+ init_pair(BLUE, COLOR_BLUE, -1);
+ init_pair(MAGENTA, COLOR_MAGENTA, -1);
+ init_pair(CYAN, COLOR_CYAN, -1);
+
+ while (true) {
+ char grid[X][Y] = {};
+ struct enemies enemies = {};
+ struct turrets spawned_turrets = {};
+
+ int cash = STARTING_CASH;
+ int lives = STARTING_LIVES;
+ int round = STARTING_ROUND;
+ int score = 0;
+
+ generate_path(grid, &enemies);
+
+ bool paused = true;
+ int done = RUNNING;
+ while (!done) {
+ if (!paused) {
+ int deaths = spawn_enemies(&enemies, grid, round);
+ lives -= deaths;
+
+ if (no_enemies(&enemies)) {
+ round++;
+ }
+
+ if (lives < 0) {
+ done = GAME_OVER;
+ }
+
+ int killed = run_turrets(&spawned_turrets, &enemies, grid);
+ if (killed >= 0) {
+ cash += killed;
+ score += killed;
+ }
+ }
+
+ erase();
+ draw_grid(grid);
+ draw_enemies(&enemies);
+ draw_shop(cash);
+ move(Y + 2, 0);
+ printw("Round: %d\n", round);
+ printw("Lives: %d\n", lives);
+ printw("Cash: %d\n", cash);
+ printw("Score: %d\n", score);
+ move(Y + 2, X + 2 - 9);
+ addstr("q to quit");
+ move(Y + 3, X + 2 - 14);
+ if (paused) addstr(" Any to resume");
+ else addstr("Space to pause");
+ refresh();
+
+ switch (getch()) {
+ case 'q': done = QUIT; break;
+ case ' ':
+ paused = !paused;
+ break;
+ case KEY_MOUSE:;
+ MEVENT e;
+ if (getmouse(&e) != OK) break;
+
+ int id = yx_to_shop_id(e.y, e.x);
+ if (id >= 0) {
+ int cost = try_purchase(grid, id, cash, &spawned_turrets);
+ if (cost < 0) {
+ move(Y/2 + 1, X/2 + 1 - 9);
+ attron(A_REVERSE);
+ addstr("Insufficient Funds");
+ attroff(A_REVERSE);
+ refresh();
+ napms(500);
+ } else {
+ cash -= cost;
+ }
+ } else {
+ int tid = -1;
+ int x, y;
+ for (int i = 0; i < spawned_turrets.idx; i++) {
+ x = spawned_turrets.spawned[i].x;
+ y = spawned_turrets.spawned[i].y;
+ if (x != e.x - 1 && y != e.y - 1) continue;
+
+ tid = i;
+ break;
+ }
+
+ if (tid >= 0) {
+ int cost = try_upgrade(tid, cash, &spawned_turrets, grid);
+ cash -= cost;
+ // turret was sold, remove it
+ if (cost < 0) {
+ turrets_pop(&spawned_turrets, grid, x, y, tid);
+ }
+ }
+ }
+ break;
+ case ERR: break;
+ default:
+ if (paused) paused = false;
+ break;
+ }
+
+ ticks++;
+ }
+
+ switch (done) {
+ case QUIT: goto terminate;
+ case GAME_OVER:
+ move(Y/2 + 1, X/2 + 1 - 12);
+ addch(' ');
+ attron(A_REVERSE);
+ addstr("You ran out of lives :(");
+ attroff(A_REVERSE);
+ addch(' ');
+ move(Y/2 + 2, X/2 + 1 - 12);
+ addch(' ');
+ attron(A_REVERSE);
+ printw("Final Score: %9d ", score);
+ attroff(A_REVERSE);
+ addch(' ');
+ break;
+ }
+
+ move(Y/2 + 3, X/2 + 1 - 14);
+ addch(' ');
+ attron(A_REVERSE);
+ addstr("Any to play again, q to quit");
+ attroff(A_REVERSE);
+ addch(' ');
+ timeout(-1);
+ if (getch() == 'q') goto terminate;
+ timeout(DELAY);
+ }
+
+ terminate:
+ keypad(stdscr, FALSE);
+ curs_set(1);
+ echo();
+ endwin();
+}