759 lines
21 KiB
C
759 lines
21 KiB
C
/**
|
||
* @file game.c
|
||
* @brief Tiny “Worms-like” artillery game for a 128x64 monochrome OLED.
|
||
*
|
||
* Coordinate system:
|
||
* - (0,0) is the top-left corner of the OLED framebuffer.
|
||
* - X increases to the right, Y increases downward.
|
||
*
|
||
* Gameplay (very simple):
|
||
* - Two worms stand on a 1D heightfield terrain (ground_y[x]).
|
||
* - On your turn you can move left/right, adjust aim angle, choose weapon, then shoot.
|
||
* - First press on FIRE enters “power charge” mode (power oscillates 0..1).
|
||
* - Second press on FIRE fires the shot with the current power.
|
||
* - Projectile flies with gravity (and optional wind), explodes on terrain or worm hit.
|
||
* - Explosion creates a crater and deals radial damage.
|
||
* - When a player reaches 0 HP the game shows GAME OVER.
|
||
*
|
||
* Controls:
|
||
* - 2: aim up (increase angle)
|
||
* - 8: aim down (decrease angle)
|
||
* - 4: move left
|
||
* - 6: move right
|
||
* - 5: FIRE (first press -> start charging, second press -> shoot)
|
||
* - *: next weapon (only while aiming)
|
||
* - #: previous weapon (only while aiming)
|
||
* - 0: restart (always available)
|
||
*
|
||
* Timing:
|
||
* - Runs as a FreeRTOS task at a fixed frame period (FRAME_MS).
|
||
* - Physics integration uses dt = FRAME_MS / 1000.0f (simple Euler integration).
|
||
*/
|
||
|
||
#include "game.h"
|
||
#include "main.h"
|
||
|
||
#include "oled.h"
|
||
#include "fonts.h"
|
||
#include "kb.h"
|
||
|
||
#include "FreeRTOS.h"
|
||
#include "task.h"
|
||
|
||
#include <math.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#ifndef OLED_W
|
||
#define OLED_W OLED_WIDTH
|
||
#endif
|
||
|
||
#ifndef OLED_H
|
||
#define OLED_H OLED_HEIGHT
|
||
#endif
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Key reading */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
/**
|
||
* @brief Read keypad and translate to a single “key id”.
|
||
*
|
||
* kb.c returns a 3-bit pattern for each scanned row (0x04/0x02/0x01),
|
||
* where only one of the three bits can become “active” for a pressed key.
|
||
*
|
||
* @return
|
||
* - 1..9, 0 for digits
|
||
* - -1 for '*', -2 for '#'
|
||
* - -100 if nothing is pressed
|
||
*/
|
||
static int8_t get_key(void)
|
||
{
|
||
uint8_t k;
|
||
|
||
k = Check_Row(ROW1);
|
||
if (k == 0x04) return 1;
|
||
if (k == 0x02) return 2;
|
||
if (k == 0x01) return 3;
|
||
|
||
k = Check_Row(ROW2);
|
||
if (k == 0x04) return 4;
|
||
if (k == 0x02) return 5;
|
||
if (k == 0x01) return 6;
|
||
|
||
k = Check_Row(ROW3);
|
||
if (k == 0x04) return 7;
|
||
if (k == 0x02) return 8;
|
||
if (k == 0x01) return 9;
|
||
|
||
k = Check_Row(ROW4);
|
||
if (k == 0x04) return -1; /* '*' */
|
||
if (k == 0x02) return 0; /* '0' */
|
||
if (k == 0x01) return -2; /* '#' */
|
||
|
||
return -100;
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Game model */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
typedef enum {
|
||
GS_AIM = 0, /* player can move/aim/select weapon */
|
||
GS_CHARGE, /* power bar oscillates until second FIRE */
|
||
GS_FLIGHT, /* projectile is flying */
|
||
GS_EXPLOSION, /* explosion ring animation */
|
||
GS_GAMEOVER
|
||
} GameState;
|
||
|
||
typedef struct {
|
||
const char *name;
|
||
uint8_t blast_radius; /* pixels */
|
||
uint8_t max_damage; /* HP at distance 0 */
|
||
float vmin, vmax; /* projectile speed range (px/s) */
|
||
} Weapon;
|
||
|
||
static const Weapon weapons[] = {
|
||
{ "NORM", 10, 40, 35.0f, 70.0f },
|
||
{ "BIG ", 14, 60, 30.0f, 65.0f },
|
||
{ "MINI", 8, 25, 40.0f, 80.0f },
|
||
};
|
||
#define WEAPON_COUNT ((uint8_t)(sizeof(weapons) / sizeof(weapons[0])))
|
||
|
||
typedef struct {
|
||
int x; /* pixels */
|
||
int y; /* pixels (worm “body center”) */
|
||
int hp; /* 0..100 */
|
||
int angle_deg; /* 0..180, where 0 = right, 90 = up, 180 = left */
|
||
uint8_t weapon_id;
|
||
} Worm;
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* World state */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
/**
|
||
* ground_y[x] = first ground pixel Y at column x.
|
||
* Everything with y >= ground_y[x] is filled as terrain.
|
||
*/
|
||
static uint8_t ground_y[OLED_W];
|
||
|
||
static Worm worms[2];
|
||
static uint8_t current_player = 0;
|
||
static GameState gs = GS_AIM;
|
||
|
||
/* Charge state (used only in GS_CHARGE) */
|
||
static float charge_power = 0.0f; /* 0..1 */
|
||
static float charge_dir = 1.0f; /* +1 or -1, causes oscillation */
|
||
|
||
/* Projectile state (used only in GS_FLIGHT) */
|
||
static float proj_x, proj_y;
|
||
static float proj_vx, proj_vy;
|
||
static uint8_t proj_owner;
|
||
static float proj_age; /* seconds since fired */
|
||
|
||
/* Explosion state (used only in GS_EXPLOSION) */
|
||
static int expl_x, expl_y;
|
||
static uint8_t expl_r, expl_rmax;
|
||
|
||
/* Physics parameters (in “pixel units”) */
|
||
static const float GRAVITY = 90.0f; /* px/s^2 */
|
||
static float WIND = 0.0f; /* px/s^2, applied to vx each update */
|
||
|
||
/* Fixed frame period */
|
||
#define FRAME_MS 30
|
||
|
||
static const float PI = 3.1415926f;
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Drawing helpers */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
static inline void px(int x, int y, uint8_t color)
|
||
{
|
||
if (x < 0 || x >= OLED_W || y < 0 || y >= OLED_H) return;
|
||
oled_DrawPixel((uint8_t)x, (uint8_t)y, color);
|
||
}
|
||
|
||
/* Integer Bresenham line */
|
||
static void line(int x0, int y0, int x1, int y1, uint8_t color)
|
||
{
|
||
int dx = (x1 > x0) ? (x1 - x0) : (x0 - x1);
|
||
int sx = (x0 < x1) ? 1 : -1;
|
||
int dy = (y1 > y0) ? (y0 - y1) : (y1 - y0);
|
||
int sy = (y0 < y1) ? 1 : -1;
|
||
int err = dx + dy;
|
||
|
||
while (1) {
|
||
px(x0, y0, color);
|
||
if (x0 == x1 && y0 == y1) break;
|
||
int e2 = 2 * err;
|
||
if (e2 >= dy) { err += dy; x0 += sx; }
|
||
if (e2 <= dx) { err += dx; y0 += sy; }
|
||
}
|
||
}
|
||
|
||
/* Midpoint circle outline */
|
||
static void circle(int cx, int cy, int r, uint8_t color)
|
||
{
|
||
int x = r, y = 0, err = 0;
|
||
|
||
while (x >= y) {
|
||
px(cx + x, cy + y, color);
|
||
px(cx + y, cy + x, color);
|
||
px(cx - y, cy + x, color);
|
||
px(cx - x, cy + y, color);
|
||
px(cx - x, cy - y, color);
|
||
px(cx - y, cy - x, color);
|
||
px(cx + y, cy - x, color);
|
||
px(cx + x, cy - y, color);
|
||
|
||
y++;
|
||
err += 1 + 2 * y;
|
||
if (2 * (err - x) + 1 > 0) {
|
||
x--;
|
||
err += 1 - 2 * x;
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Small filled circle (O(r^2), OK for small r) */
|
||
static void filled_circle(int cx, int cy, int r, uint8_t color)
|
||
{
|
||
for (int yy = -r; yy <= r; yy++) {
|
||
for (int xx = -r; xx <= r; xx++) {
|
||
if (xx * xx + yy * yy <= r * r) px(cx + xx, cy + yy, color);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Utilities */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
static int clampi(int v, int lo, int hi)
|
||
{
|
||
if (v < lo) return lo;
|
||
if (v > hi) return hi;
|
||
return v;
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Terrain */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
/**
|
||
* @brief Generate a simple “random walk” heightfield.
|
||
*
|
||
* The terrain is represented as a height per X column. This is cheap to draw and
|
||
* cheap to crater (modify only affected columns).
|
||
*/
|
||
static void terrain_generate(void)
|
||
{
|
||
int y = 50;
|
||
uint32_t s = 1234567u; /* fixed seed: deterministic terrain each boot */
|
||
|
||
for (int x = 0; x < OLED_W; x++) {
|
||
s = s * 1103515245u + 12345u;
|
||
int n = (int)((s >> 28) & 0x0F) - 7; /* [-7..8] */
|
||
|
||
/* Turn into a small step up/down/flat to keep terrain smooth-ish */
|
||
y += (n > 0 ? 1 : (n < 0 ? -1 : 0));
|
||
|
||
/* Clamp to keep playable area */
|
||
if (y < 35) y = 35;
|
||
if (y > 58) y = 58;
|
||
|
||
ground_y[x] = (uint8_t)y;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Place worm at X and snap its Y a few pixels above ground.
|
||
*/
|
||
static void place_worm(uint8_t id, int x)
|
||
{
|
||
x = clampi(x, 2, OLED_W - 3);
|
||
int y = (int)ground_y[x] - 3; /* stand above ground */
|
||
worms[id].x = x;
|
||
worms[id].y = y;
|
||
}
|
||
|
||
/**
|
||
* @brief Create a crater by pushing the terrain surface downward.
|
||
*
|
||
* This does not “remove” ground pixels (it increases ground_y = lower surface),
|
||
* which visually creates a hole.
|
||
*/
|
||
static void crater(int cx, int r)
|
||
{
|
||
int x0 = clampi(cx - r, 0, OLED_W - 1);
|
||
int x1 = clampi(cx + r, 0, OLED_W - 1);
|
||
|
||
for (int x = x0; x <= x1; x++) {
|
||
int dx = x - cx;
|
||
int rr2 = r * r;
|
||
int d2 = dx * dx;
|
||
if (d2 > rr2) continue;
|
||
|
||
/* Circular profile, scaled a bit to keep crater shallow */
|
||
int drop = (int)(sqrtf((float)(rr2 - d2)) * 0.55f);
|
||
int ny = (int)ground_y[x] + drop;
|
||
|
||
if (ny > (OLED_H - 1)) ny = (OLED_H - 1);
|
||
ground_y[x] = (uint8_t)ny;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Apply radial damage to worms inside explosion radius.
|
||
*
|
||
* Damage linearly decreases with distance:
|
||
* dmg = max_damage * (1 - dist/r)
|
||
*/
|
||
static void apply_damage(int cx, int cy, uint8_t r, uint8_t maxdmg)
|
||
{
|
||
for (int i = 0; i < 2; i++) {
|
||
int dx = worms[i].x - cx;
|
||
int dy = worms[i].y - cy;
|
||
float dist = sqrtf((float)(dx * dx + dy * dy));
|
||
if (dist > (float)r) continue;
|
||
|
||
float k = 1.0f - (dist / (float)r);
|
||
int dmg = (int)(k * (float)maxdmg);
|
||
if (dmg < 0) dmg = 0;
|
||
|
||
worms[i].hp -= dmg;
|
||
if (worms[i].hp < 0) worms[i].hp = 0;
|
||
}
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Rendering */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
static void draw_terrain(void)
|
||
{
|
||
for (int x = 0; x < OLED_W; x++) {
|
||
for (int y = ground_y[x]; y < OLED_H; y++) px(x, y, White);
|
||
}
|
||
}
|
||
|
||
static void draw_worm(int x, int y, uint8_t active)
|
||
{
|
||
/* Active worm is filled so it’s easy to see whose turn it is */
|
||
if (active) {
|
||
filled_circle(x, y, 2, White);
|
||
px(x, y - 3, White);
|
||
} else {
|
||
circle(x, y, 2, White);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* @brief Draw a short dotted trajectory preview.
|
||
*
|
||
* This is intentionally short and sparse (like the classic Worms preview),
|
||
* so it fits the small screen and doesn’t clutter.
|
||
*/
|
||
static void draw_short_trajectory_preview(const Worm *w, float pwr01)
|
||
{
|
||
const int PREVIEW_PTS = 14;
|
||
const float dt = 0.06f;
|
||
|
||
const Weapon *wp = &weapons[w->weapon_id];
|
||
|
||
float speed = wp->vmin + pwr01 * (wp->vmax - wp->vmin);
|
||
float rad = (float)w->angle_deg * PI / 180.0f;
|
||
|
||
float vx = cosf(rad) * speed;
|
||
float vy = -sinf(rad) * speed;
|
||
|
||
float x = (float)w->x;
|
||
float y = (float)w->y;
|
||
|
||
for (int i = 0; i < PREVIEW_PTS; i++) {
|
||
x += vx * dt;
|
||
y += vy * dt;
|
||
vy += GRAVITY * dt;
|
||
|
||
int xi = (int)(x + 0.5f);
|
||
int yi = (int)(y + 0.5f);
|
||
|
||
if (xi < 0 || xi >= OLED_W || yi < 0 || yi >= OLED_H) break;
|
||
if (yi >= (int)ground_y[xi]) break;
|
||
|
||
if ((i & 1) == 0) px(xi, yi, White);
|
||
}
|
||
}
|
||
|
||
static void draw_ui(void)
|
||
{
|
||
char s[32];
|
||
|
||
oled_SetCursor(0, 0);
|
||
sprintf(s, "P1:%3d", worms[0].hp);
|
||
oled_WriteString(s, Font_7x10, White);
|
||
|
||
oled_SetCursor(64, 0);
|
||
sprintf(s, "P2:%3d", worms[1].hp);
|
||
oled_WriteString(s, Font_7x10, White);
|
||
|
||
const Worm *w = &worms[current_player];
|
||
oled_SetCursor(0, 12);
|
||
sprintf(s, "W:%s A:%3d", weapons[w->weapon_id].name, w->angle_deg);
|
||
oled_WriteString(s, Font_7x10, White);
|
||
|
||
/* Power bar: only shown while charging */
|
||
if (gs == GS_CHARGE) {
|
||
int bar_x = 4, bar_y = 56, bar_w = 120, bar_h = 6;
|
||
|
||
line(bar_x, bar_y, bar_x + bar_w, bar_y, White);
|
||
line(bar_x, bar_y + bar_h, bar_x + bar_w, bar_y + bar_h, White);
|
||
line(bar_x, bar_y, bar_x, bar_y + bar_h, White);
|
||
line(bar_x + bar_w, bar_y, bar_x + bar_w, bar_y + bar_h, White);
|
||
|
||
int fill = (int)(charge_power * (float)(bar_w - 2));
|
||
fill = clampi(fill, 0, bar_w - 2);
|
||
|
||
for (int x = 0; x < fill; x++) {
|
||
for (int y = 0; y < bar_h - 1; y++) {
|
||
px(bar_x + 1 + x, bar_y + 1 + y, White);
|
||
}
|
||
}
|
||
}
|
||
|
||
if (gs == GS_GAMEOVER) {
|
||
oled_SetCursor(20, 30);
|
||
if (worms[0].hp == 0 && worms[1].hp == 0) {
|
||
oled_WriteString("DRAW", Font_11x18, White);
|
||
} else if (worms[0].hp == 0) {
|
||
oled_WriteString("P2 WINS", Font_11x18, White);
|
||
} else {
|
||
oled_WriteString("P1 WINS", Font_11x18, White);
|
||
}
|
||
|
||
oled_SetCursor(6, 52);
|
||
oled_WriteString("0=restart", Font_7x10, White);
|
||
}
|
||
}
|
||
|
||
static void render(void)
|
||
{
|
||
oled_Fill(Black);
|
||
|
||
draw_terrain();
|
||
|
||
draw_worm(worms[0].x, worms[0].y, (current_player == 0 && gs != GS_GAMEOVER));
|
||
draw_worm(worms[1].x, worms[1].y, (current_player == 1 && gs != GS_GAMEOVER));
|
||
|
||
/* Aim line + trajectory preview while aiming/charging */
|
||
if (gs == GS_AIM || gs == GS_CHARGE) {
|
||
Worm *w = &worms[current_player];
|
||
float rad = (float)w->angle_deg * PI / 180.0f;
|
||
|
||
int x2 = w->x + (int)(cosf(rad) * 10.0f);
|
||
int y2 = w->y - (int)(sinf(rad) * 10.0f);
|
||
line(w->x, w->y, x2, y2, White);
|
||
|
||
/* During AIM we preview with a “typical” mid power; during CHARGE show real power */
|
||
float p = (gs == GS_CHARGE) ? charge_power : 0.55f;
|
||
draw_short_trajectory_preview(w, p);
|
||
}
|
||
|
||
if (gs == GS_FLIGHT) {
|
||
px((int)(proj_x + 0.5f), (int)(proj_y + 0.5f), White);
|
||
}
|
||
|
||
if (gs == GS_EXPLOSION) {
|
||
circle(expl_x, expl_y, expl_r, White);
|
||
}
|
||
|
||
draw_ui();
|
||
oled_UpdateScreen();
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Game flow */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
static void next_player(void)
|
||
{
|
||
current_player ^= 1u;
|
||
gs = GS_AIM;
|
||
|
||
/* Reset charge state for the next turn */
|
||
charge_power = 0.0f;
|
||
charge_dir = 1.0f;
|
||
}
|
||
|
||
/**
|
||
* @brief Start explosion: apply crater + damage instantly, then animate ring.
|
||
*/
|
||
static void explode_at(int x, int y)
|
||
{
|
||
Worm *w = &worms[current_player];
|
||
const Weapon *wp = &weapons[w->weapon_id];
|
||
|
||
expl_x = clampi(x, 0, OLED_W - 1);
|
||
expl_y = clampi(y, 0, OLED_H - 1);
|
||
expl_r = 1;
|
||
expl_rmax = wp->blast_radius;
|
||
|
||
crater(expl_x, wp->blast_radius);
|
||
apply_damage(expl_x, expl_y, wp->blast_radius, wp->max_damage);
|
||
|
||
if (worms[0].hp == 0 || worms[1].hp == 0) {
|
||
gs = GS_GAMEOVER;
|
||
return;
|
||
}
|
||
|
||
gs = GS_EXPLOSION;
|
||
}
|
||
|
||
static void start_shot(void)
|
||
{
|
||
Worm *w = &worms[current_player];
|
||
const Weapon *wp = &weapons[w->weapon_id];
|
||
|
||
float speed = wp->vmin + charge_power * (wp->vmax - wp->vmin);
|
||
float rad = (float)w->angle_deg * PI / 180.0f;
|
||
|
||
/* Spawn projectile from the “muzzle”, not the worm center.
|
||
* This avoids immediate self-collision due to rounding.
|
||
*/
|
||
const float MUZZLE = 7.0f;
|
||
|
||
proj_x = (float)w->x + cosf(rad) * MUZZLE;
|
||
proj_y = (float)w->y - sinf(rad) * MUZZLE;
|
||
|
||
proj_vx = cosf(rad) * speed;
|
||
proj_vy = -sinf(rad) * speed;
|
||
|
||
proj_owner = current_player;
|
||
proj_age = 0.0f;
|
||
|
||
gs = GS_FLIGHT;
|
||
}
|
||
|
||
static void update_charge(void)
|
||
{
|
||
/* Power oscillates between 0 and 1, so the player “times” the second FIRE. */
|
||
const float step = 0.035f;
|
||
charge_power += charge_dir * step;
|
||
|
||
if (charge_power >= 1.0f) { charge_power = 1.0f; charge_dir = -1.0f; }
|
||
if (charge_power <= 0.0f) { charge_power = 0.0f; charge_dir = 1.0f; }
|
||
}
|
||
|
||
static void update_flight(float dt)
|
||
{
|
||
proj_age += dt;
|
||
|
||
/* Simple Euler integration */
|
||
proj_vx += WIND * dt;
|
||
proj_vy += GRAVITY * dt;
|
||
|
||
proj_x += proj_vx * dt;
|
||
proj_y += proj_vy * dt;
|
||
|
||
int xi = (int)(proj_x + 0.5f);
|
||
int yi = (int)(proj_y + 0.5f);
|
||
|
||
/* If we leave the visible screen, explode at the nearest edge point */
|
||
if (xi < 0 || xi >= OLED_W || yi < 0 || yi >= OLED_H) {
|
||
explode_at(clampi(xi, 0, OLED_W - 1), clampi(yi, 0, OLED_H - 1));
|
||
return;
|
||
}
|
||
|
||
/* Terrain hit: explode right on the terrain surface at this column */
|
||
if (yi >= (int)ground_y[xi]) {
|
||
explode_at(xi, (int)ground_y[xi]);
|
||
return;
|
||
}
|
||
|
||
/* Worm collision: treat each worm as a small circle around (x,y) */
|
||
for (int i = 0; i < 2; i++) {
|
||
|
||
/* Ignore the shooter for a short time to avoid instant self-hit from rounding.
|
||
* (Especially important because the projectile is rendered as a single pixel.)
|
||
*/
|
||
if ((uint8_t)i == proj_owner && proj_age < 0.12f) continue;
|
||
|
||
int dx = worms[i].x - xi;
|
||
int dy = worms[i].y - yi;
|
||
|
||
if (dx * dx + dy * dy <= 9) { /* radius ~3 px */
|
||
explode_at(xi, yi);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
static void update_explosion(void)
|
||
{
|
||
/* Expand ring until max radius, then hand the turn to the other player */
|
||
if (expl_r < expl_rmax) {
|
||
expl_r += 2;
|
||
if (expl_r > expl_rmax) expl_r = expl_rmax;
|
||
} else {
|
||
next_player();
|
||
}
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Weapons */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
static void weapon_next(void)
|
||
{
|
||
Worm *w = &worms[current_player];
|
||
w->weapon_id = (uint8_t)((w->weapon_id + 1u) % WEAPON_COUNT);
|
||
}
|
||
|
||
static void weapon_prev(void)
|
||
{
|
||
Worm *w = &worms[current_player];
|
||
w->weapon_id = (uint8_t)((w->weapon_id + WEAPON_COUNT - 1u) % WEAPON_COUNT);
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Input handling */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
static void handle_key_press(int8_t key)
|
||
{
|
||
/* Restart is always allowed */
|
||
if (key == 0) {
|
||
terrain_generate();
|
||
|
||
worms[0].hp = 100; worms[1].hp = 100;
|
||
worms[0].angle_deg = 45; worms[1].angle_deg = 135;
|
||
worms[0].weapon_id = 0; worms[1].weapon_id = 0;
|
||
|
||
place_worm(0, 12);
|
||
place_worm(1, OLED_W - 13);
|
||
|
||
current_player = 0;
|
||
gs = GS_AIM;
|
||
charge_power = 0.0f;
|
||
charge_dir = 1.0f;
|
||
|
||
return;
|
||
}
|
||
|
||
if (gs == GS_GAMEOVER) return;
|
||
|
||
/* Weapon switch only makes sense when not charging/flying/exploding */
|
||
if (key == -1) { /* '*' */
|
||
if (gs == GS_AIM) weapon_next();
|
||
return;
|
||
}
|
||
if (key == -2) { /* '#' */
|
||
if (gs == GS_AIM) weapon_prev();
|
||
return;
|
||
}
|
||
|
||
if (gs == GS_AIM) {
|
||
Worm *w = &worms[current_player];
|
||
|
||
if (key == 2) { /* aim up */
|
||
w->angle_deg += 2;
|
||
if (w->angle_deg > 180) w->angle_deg = 180;
|
||
} else if (key == 8) { /* aim down */
|
||
w->angle_deg -= 2;
|
||
if (w->angle_deg < 0) w->angle_deg = 0;
|
||
} else if (key == 4) { /* move left */
|
||
place_worm(current_player, w->x - 1);
|
||
} else if (key == 6) { /* move right */
|
||
place_worm(current_player, w->x + 1);
|
||
} else if (key == 5) { /* FIRE: first press enters charge mode */
|
||
gs = GS_CHARGE;
|
||
charge_power = 0.0f;
|
||
charge_dir = 1.0f;
|
||
}
|
||
} else if (gs == GS_CHARGE) {
|
||
/* FIRE again to commit the power and shoot */
|
||
if (key == 5) {
|
||
start_shot();
|
||
}
|
||
}
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* FreeRTOS task */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
/**
|
||
* @brief Main game loop task.
|
||
*
|
||
* Input model:
|
||
* - Uses edge detection: a key press is handled once per press (no repeat).
|
||
* - If you want auto-repeat for movement/aim, you can add a repeat timer.
|
||
*/
|
||
static void GameTask(void *argument)
|
||
{
|
||
(void)argument;
|
||
|
||
int8_t last_key = -100;
|
||
|
||
TickType_t last_wake = xTaskGetTickCount();
|
||
const TickType_t period = pdMS_TO_TICKS(FRAME_MS);
|
||
|
||
for (;;) {
|
||
/* Edge detect: only handle transitions to a pressed key */
|
||
int8_t key = get_key();
|
||
if (key != last_key && key != -100) {
|
||
handle_key_press(key);
|
||
}
|
||
last_key = key;
|
||
|
||
/* Update state machine */
|
||
if (gs == GS_CHARGE) {
|
||
update_charge();
|
||
} else if (gs == GS_FLIGHT) {
|
||
update_flight((float)FRAME_MS / 1000.0f);
|
||
} else if (gs == GS_EXPLOSION) {
|
||
update_explosion();
|
||
}
|
||
|
||
render();
|
||
|
||
vTaskDelayUntil(&last_wake, period);
|
||
}
|
||
}
|
||
|
||
/* -------------------------------------------------------------------------- */
|
||
/* Public API */
|
||
/* -------------------------------------------------------------------------- */
|
||
|
||
void Game_Init(void)
|
||
{
|
||
terrain_generate();
|
||
|
||
worms[0].hp = 100;
|
||
worms[1].hp = 100;
|
||
|
||
worms[0].angle_deg = 45;
|
||
worms[1].angle_deg = 135;
|
||
|
||
worms[0].weapon_id = 0;
|
||
worms[1].weapon_id = 0;
|
||
|
||
place_worm(0, 12);
|
||
place_worm(1, OLED_W - 13);
|
||
|
||
current_player = 0;
|
||
gs = GS_AIM;
|
||
|
||
charge_power = 0.0f;
|
||
charge_dir = 1.0f;
|
||
|
||
/* Wind is supported by the physics, currently disabled (0). */
|
||
WIND = 0.0f;
|
||
|
||
render();
|
||
|
||
(void)xTaskCreate(GameTask, "Game", 512, NULL, tskIDLE_PRIORITY + 2, NULL);
|
||
}
|