From be888397e4b742f477162067e53072b2b5b4fb8b Mon Sep 17 00:00:00 2001 From: Domenico Testa Date: Wed, 11 Feb 2026 12:14:20 +0100 Subject: [PATCH] feat: initial revision --- src/main.c | 624 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 606 insertions(+), 18 deletions(-) diff --git a/src/main.c b/src/main.c index 0f6a14b..761bad7 100644 --- a/src/main.c +++ b/src/main.c @@ -1,50 +1,638 @@ /* -Raylib example file. -This is an example main file for a simple raylib project. -Use this as a starting point or replace it with your code. - -by Jeffery Myers is marked with CC0 1.0. To view a copy of this license, visit https://creativecommons.org/publicdomain/zero/1.0/ - +Shooting arrows - ya know */ +#include +#include +#include +#include + #include "raylib.h" +#include "resource_dir.h" -#include "resource_dir.h" // utility header for SearchAndSetResourceDir +const float G = 458.0; +const float attritionCoefficient = 95.0; +const int archerSize = 10; +const float archerSpeed = 96.0; +const float maxBowTension = 921.0; +const float bowTensionSpeed = 12.0; -int main () +const float spacing = 16; +const float bowTensionIndicatorWidth = 360; +const float bowTensionIndicatorHeight = 40; + +const float arrowLength = 20.0f; +const int groundHeight = 40; + +#define MAX_BLOOD_PARTICLES 800 + +// Utility function to map a value from one range to another +float map(float value, float fromLow, float fromHigh, float toLow, float toHigh) +{ + return toLow + (toHigh - toLow) * (value - fromLow) / (fromHigh - fromLow); +} + +// Utility function to normalize a Vector2 +Vector2 normalized(Vector2 v) +{ + float len = sqrtf(v.x * v.x + v.y * v.y); + if (len == 0.0f) + { + Vector2 zero = {0.0f, 0.0f}; + return zero; + } + Vector2 n = {v.x / len, v.y / len}; + return n; +} + +typedef struct Target Target; +typedef struct Arrow Arrow; + +struct Arrow +{ + Vector2 position; + Vector2 speed; + Vector2 acceleration; + Vector2 direction; + bool stuck; + Target *hitTarget; +}; + +typedef struct ArrowNode ArrowNode; + +struct ArrowNode +{ + Arrow *data; + ArrowNode *prev; + ArrowNode *next; +}; + +// Bleed pattern: alternating durations of squirt (spawn particles) and pause +// {squirt, pause, squirt, pause, squirt, pause, squirt,..., 0} — 0 marks end +#define BLEED_PHASES 18 +const float bleedPattern[BLEED_PHASES] = {0.08f, 0.06f, 0.38f, 0.25f, 0.08f, 2.06f, 0.38f, 0.25f, 0.10f, 1.06f, 0.08f, 0.3f, 2.2f, 0.4f, 1.1f, 0.9f, 0.8f, 0.0f}; +// even indices = squirt, odd indices = pause, last 0 = done + +typedef struct +{ + Vector2 position; + Vector2 velocity; + float lifetime; + float maxLifetime; + bool active; +} BloodParticle; + +BloodParticle bloodParticles[MAX_BLOOD_PARTICLES]; + +void spawn_blood(Vector2 origin, Vector2 direction, int count) +{ + for (int i = 0; i < MAX_BLOOD_PARTICLES && count > 0; i++) + { + if (!bloodParticles[i].active) + { + bloodParticles[i].active = true; + bloodParticles[i].position = origin; + // base direction is opposite to the arrow's flight + float baseAngle = atan2f(-direction.y, -direction.x); + // spread within ~90 degrees of the opposite direction + float spread = ((float)GetRandomValue(-45, 45)) * DEG2RAD; + float angle = baseAngle + spread; + float speed = (float)GetRandomValue(0, 320); + bloodParticles[i].velocity.x = cosf(angle) * speed; + bloodParticles[i].velocity.y = sinf(angle) * speed; + bloodParticles[i].maxLifetime = 0.4f + (float)GetRandomValue(10, 475) / 100.0f; + bloodParticles[i].lifetime = bloodParticles[i].maxLifetime; + count--; + } + } +} + +void update_blood(float deltaTime) +{ + for (int i = 0; i < MAX_BLOOD_PARTICLES; i++) + { + if (!bloodParticles[i].active) + continue; + + bloodParticles[i].lifetime -= deltaTime; + if (bloodParticles[i].lifetime <= 0) + { + bloodParticles[i].active = false; + continue; + } + + bloodParticles[i].velocity.y += G * 0.5f * deltaTime; + bloodParticles[i].position.x += bloodParticles[i].velocity.x * deltaTime; + bloodParticles[i].position.y += bloodParticles[i].velocity.y * deltaTime; + } +} + +void draw_blood(void) +{ + for (int i = 0; i < MAX_BLOOD_PARTICLES; i++) + { + if (!bloodParticles[i].active) + continue; + + float alpha = bloodParticles[i].lifetime / bloodParticles[i].maxLifetime; + unsigned char a = (unsigned char)(alpha * 255); + Color c = {200, 0, 0, a}; + float size = 2.0f + alpha * 2.0f; + DrawRectangle( + (int)(bloodParticles[i].position.x - size / 2), + (int)(bloodParticles[i].position.y - size / 2), + (int)size, (int)size, c); + } +} + +struct Target +{ + Vector2 position; + Vector2 speed; + bool bleeding; + bool grounded; + int bleedPhase; + float bleedTimer; +}; + +typedef struct TargetNode TargetNode; + +struct TargetNode +{ + Target *data; + TargetNode *prev; + TargetNode *next; +}; + +void add_target(TargetNode *head, Vector2 position) +{ + TargetNode *el = head; + while (el->next) + { + el = el->next; + } + + Target *t = malloc(sizeof(Target)); + t->position = position; + t->speed.x = 0; + t->speed.y = 0; + t->bleeding = false; + t->grounded = false; + t->bleedPhase = 0; + t->bleedTimer = 0; + + TargetNode *newNode = (TargetNode *)malloc(sizeof(TargetNode)); + newNode->data = t; + newNode->prev = el; + newNode->next = NULL; + + el->next = newNode; +} + +void remove_target(TargetNode *node) +{ + node->prev->next = node->next; + if (node->next) + node->next->prev = node->prev; + free(node->data); + free(node); +} + +void fire_arrow(ArrowNode *head, Vector2 origin, Vector2 acceleration) +{ + if (!head) + { + TraceLog(LOG_ERROR, "Cannot fire arrow: head pointer is NULL"); + return; + } + + ArrowNode *el = head; + while (el->next) + { + el = el->next; + } + + Arrow *a = malloc(sizeof(Arrow)); + a->position.x = origin.x; + a->position.y = origin.y; + a->speed.x = acceleration.x; + a->speed.y = acceleration.y; + a->acceleration.x = 0; + a->acceleration.y = 0; + a->direction = normalized(a->speed); + a->stuck = false; + a->hitTarget = NULL; + + ArrowNode *newNode = (ArrowNode *)malloc(sizeof(ArrowNode)); + newNode->data = a; + newNode->prev = el; + newNode->next = NULL; + + el->next = newNode; +} + +void update_arrows(ArrowNode *head, TargetNode *targets, Texture targetTex, float deltaTime, float attrition) +{ + Rectangle groundRect = {.x = 0, .y = GetScreenHeight() - groundHeight, .width = GetScreenWidth(), .height = groundHeight}; + + ArrowNode *el = head->next; + while (el != NULL) + { + Arrow *a = el->data; + + // skip stuck arrows + if (a->stuck) + { + el = el->next; + continue; + } + + a->speed.x += a->acceleration.x * deltaTime; + a->speed.y += a->acceleration.y * deltaTime; + + // apply gravity directly to speed + a->speed.y += G * deltaTime; + + a->position.x += a->speed.x * deltaTime; + a->position.y += a->speed.y * deltaTime; + + // update flight direction + a->direction = normalized(a->speed); + + // to simulate attrition will scale down the acceleration by a constant + float decay = powf(attrition, deltaTime); + a->acceleration.x *= decay; + a->acceleration.y *= decay; + + // check for target collisions + TargetNode *tEl = targets->next; + while (tEl != NULL) + { + Target *t = tEl->data; + if (t->bleeding) + { + tEl = tEl->next; + continue; + } + Rectangle targetRect = {t->position.x, t->position.y, (float)targetTex.width, (float)targetTex.height}; + if (CheckCollisionPointRec(a->position, targetRect)) + { + t->bleeding = true; + t->bleedPhase = 0; + t->bleedTimer = bleedPattern[0]; + + // penetrate into the target proportionally to hit speed + float hitSpeed = sqrtf(a->speed.x * a->speed.x + a->speed.y * a->speed.y); + float maxPenetration = arrowLength * 1.1f; + float penetration = (hitSpeed / maxBowTension) * maxPenetration; + a->position.x += a->direction.x * penetration; + a->position.y += a->direction.y * penetration; + + a->acceleration.x = a->acceleration.y = a->speed.x = a->speed.y = 0; + a->stuck = true; + a->hitTarget = t; + TraceLog(LOG_INFO, "Target hit!"); + break; + } + tEl = tEl->next; + } + + if (a->stuck) + { + el = el->next; + continue; + } + + // check for ground collision + if (CheckCollisionPointRec(a->position, groundRect)) + { + a->position.y = GetScreenHeight() - groundHeight; + a->acceleration.x = a->acceleration.y = a->speed.x = a->speed.y = 0; + a->stuck = true; + } + + // if arrow went offscreen, remove it + if ( + (a->position.x > GetScreenWidth() || a->position.x < 0) || + a->position.y > GetScreenHeight()) + { + el->prev->next = el->next; + if (el->next) + el->next->prev = el->prev; + + ArrowNode *orphan = el; + + el = el->next; + free(orphan->data); + free(orphan); + + TraceLog(LOG_INFO, "Removing arrow"); + } + else + { + el = el->next; + } + } +} + +void remove_arrow(ArrowNode *node) +{ + node->prev->next = node->next; + if (node->next) + node->next->prev = node->prev; + free(node->data); + free(node); +} + +void update_targets(TargetNode *targets, ArrowNode *arrows, Texture targetTex, float deltaTime) +{ + TargetNode *tEl = targets->next; + while (tEl != NULL) + { + Target *t = tEl->data; + if (!t->bleeding) + { + tEl = tEl->next; + continue; + } + + // apply gravity and move the target while falling + if (!t->grounded) + { + t->speed.y += G * deltaTime; + float dx = t->speed.x * deltaTime; + float dy = t->speed.y * deltaTime; + t->position.x += dx; + t->position.y += dy; + + float groundY = GetScreenHeight() - groundHeight - targetTex.height; + if (t->position.y >= groundY) + { + dy -= (t->position.y - groundY); + t->position.y = groundY; + t->speed.x = 0; + t->speed.y = 0; + t->grounded = true; + } + + // move stuck arrows along with the target + ArrowNode *aEl = arrows->next; + while (aEl != NULL) + { + if (aEl->data->hitTarget == t) + { + aEl->data->position.x += dx; + aEl->data->position.y += dy; + } + aEl = aEl->next; + } + } + + t->bleedTimer -= deltaTime; + if (t->bleedTimer > 0) + { + // during squirt phases (even indices), spawn particles + if (t->bleedPhase % 2 == 0) + { + Vector2 center = { + t->position.x + targetTex.width / 2.0f, + t->position.y + targetTex.height / 2.0f}; + + ArrowNode *aEl = arrows->next; + while (aEl != NULL) + { + ArrowNode *aNext = aEl->next; + if (aEl->data->hitTarget == t) + { + spawn_blood(aEl->data->position, aEl->data->direction, MAX_BLOOD_PARTICLES / 2); + } + aEl = aNext; + } + } + tEl = tEl->next; + } + else + { + // advance to next phase + t->bleedPhase++; + if (t->bleedPhase >= BLEED_PHASES || bleedPattern[t->bleedPhase] == 0.0f) + { + // remove arrows that hit this target + ArrowNode *aEl = arrows->next; + while (aEl != NULL) + { + ArrowNode *aNext = aEl->next; + if (aEl->data->hitTarget == t) + { + remove_arrow(aEl); + } + aEl = aNext; + } + + // animation done, remove target and spawn a new one + TargetNode *dead = tEl; + tEl = tEl->next; + remove_target(dead); + + Vector2 newPos = { + (float)GetRandomValue(0, GetScreenWidth() - targetTex.width), + (float)GetRandomValue(0, GetScreenHeight() / 2)}; + add_target(targets, newPos); + + TraceLog(LOG_INFO, "Target removed after bleeding, new target spawned"); + } + else + { + t->bleedTimer = bleedPattern[t->bleedPhase]; + tEl = tEl->next; + } + } + } +} + +int main() { // Tell the window to use vsync and work on high DPI displays - SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_HIGHDPI); + SetConfigFlags(FLAG_VSYNC_HINT | FLAG_WINDOW_HIGHDPI | FLAG_WINDOW_RESIZABLE); // Create the window and OpenGL context - InitWindow(1280, 800, "Hello Raylib"); + InitWindow(1280, 800, "Archer"); // Utility function from resource_dir.h to find the resources folder and set it as the current working directory so we can load from it SearchAndSetResourceDir("resources"); // Load a texture from the resources directory Texture wabbit = LoadTexture("wabbit_alpha.png"); - - // game loop - while (!WindowShouldClose()) // run the loop until the user presses ESCAPE or presses the Close button on the window + + int screenWidth = GetScreenWidth(); + int screenHeight = GetScreenHeight(); + + Vector2 mousePosition = {0, 0}; + char coords[200]; + + bool trajectoryVisible = false; + + Vector2 archerPosition = {screenWidth / 2 - archerSize / 2, screenHeight - groundHeight - archerSize}; + + float bowTension = 0.0; + + ArrowNode *head = (ArrowNode *)calloc(1, sizeof(ArrowNode)); + + TargetNode *targets = (TargetNode *)calloc(3, sizeof(TargetNode)); + + for (int i = 0; i < 3; i++) { + // spawn targets at a random position in the top region + Vector2 targetPos = { + (float)GetRandomValue(0, screenWidth - wabbit.width), + (float)GetRandomValue(0, screenHeight / 3)}; + add_target(targets, targetPos); + } + + float dt = 0; + + // game loop + while (!WindowShouldClose()) // run the loop until the user presses ESCAPE or presses the Close button on the window + { + dt = GetFrameTime(); + + if (IsWindowResized()) + { + screenWidth = GetScreenWidth(); + screenHeight = GetScreenHeight(); + } + + mousePosition = GetMousePosition(); + sprintf(coords, "(x=%1.0f, y=%1.0f)", mousePosition.x, mousePosition.y); + + float tensionRate = -3.0; + if (IsMouseButtonDown(MOUSE_BUTTON_LEFT)) + { + tensionRate = 1.0; + } + + bowTension += bowTensionSpeed * tensionRate; + if (bowTension < 0) + { + bowTension = 0; + } + if (bowTension > maxBowTension) + { + bowTension = maxBowTension; + } + + // Pressing the right mouse button releases the bow istantly + if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) + { + TraceLog(LOG_INFO, "Arrow launched"); + + Vector2 dir = {mousePosition.x - archerPosition.x, mousePosition.y - archerPosition.y}; + Vector2 direction = normalized(dir); + Vector2 force = {direction.x * bowTension, direction.y * bowTension}; + fire_arrow(head, archerPosition, force); + bowTension = 0; + } + + if (IsKeyPressed(KEY_T)) + { + trajectoryVisible = !trajectoryVisible; + TraceLog(LOG_INFO, "Toggling trajectory indicators"); + } + + if (IsKeyDown(KEY_A)) + { + archerPosition.x -= archerSpeed * dt; + } + + if (IsKeyDown(KEY_D)) + { + archerPosition.x += archerSpeed * dt; + } + + update_arrows(head, targets, wabbit, dt, attritionCoefficient); + update_targets(targets, head, wabbit, dt); + update_blood(dt); + // drawing BeginDrawing(); // Setup the back buffer for drawing (clear color and depth buffers) ClearBackground(BLACK); - // draw some text using the default font - DrawText("Hello Raylib", 200,200,20,WHITE); + // Draw ground + DrawRectangle(0, screenHeight - groundHeight, screenWidth, groundHeight, BROWN); + + // draw targets + TargetNode *drawTarget = targets->next; + while (drawTarget != NULL) + { + Target *t = drawTarget->data; + DrawTexture(wabbit, (int)t->position.x, (int)t->position.y, WHITE); + drawTarget = drawTarget->next; + } + + // draw blood particles + draw_blood(); + + // for now the archer will be a simple square + DrawRectangle(archerPosition.x, archerPosition.y, archerSize, archerSize, GREEN); + + // draw arrows + ArrowNode *drawEl = head->next; + while (drawEl != NULL) + { + Arrow *a = drawEl->data; + Vector2 dir = a->direction; + Vector2 tail = {a->position.x - dir.x * arrowLength, a->position.y - dir.y * arrowLength}; + DrawLineEx(tail, a->position, 2.0f, YELLOW); + drawEl = drawEl->next; + } + + if (trajectoryVisible) + { + // aim hints + Vector2 startY = {0, mousePosition.y}; + DrawLineDashed(startY, mousePosition, 4, 8, GRAY); + + Vector2 startX = {mousePosition.x, screenHeight}; + DrawLineDashed(startX, mousePosition, 4, 8, GRAY); + + Vector2 bowOrigin = {archerPosition.x + archerSize / 2, archerPosition.y + archerSize / 2}; + DrawLineDashed(bowOrigin, mousePosition, 4, 8, GRAY); + + // targeting coordinates + DrawText(coords, screenWidth - bowTensionIndicatorWidth - spacing, bowTensionIndicatorHeight + 2 * spacing, 24, WHITE); + } + + // Tension indicator + DrawRectangleLines(screenWidth - bowTensionIndicatorWidth - spacing, spacing, bowTensionIndicatorWidth, bowTensionIndicatorHeight, WHITE); + DrawRectangle(screenWidth - bowTensionIndicatorWidth - spacing, spacing, map(bowTension, 0, maxBowTension, 0, bowTensionIndicatorWidth), bowTensionIndicatorHeight, WHITE); - // draw our texture to the screen - DrawTexture(wabbit, 400, 200, WHITE); - // end the frame and get ready for the next one (display frame, poll input, etc...) EndDrawing(); } // cleanup + // free arrow linked list + ArrowNode *el = head; + while (el != NULL) + { + ArrowNode *next = el->next; + free(el->data); + free(el); + el = next; + } + + // free target linked list + TargetNode *tEl = targets; + while (tEl != NULL) + { + TargetNode *next = tEl->next; + free(tEl->data); + free(tEl); + tEl = next; + } + // unload our texture so it can be cleaned up UnloadTexture(wabbit);