feat: initial revision

This commit is contained in:
2026-02-11 12:14:20 +01:00
parent 4cf96e771f
commit be888397e4

View File

@@ -1,23 +1,462 @@
/* /*
Raylib example file. Shooting arrows - ya know
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/
*/ */
#include <stdbool.h>
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include "raylib.h" #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 // 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 // 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 // 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"); SearchAndSetResourceDir("resources");
@@ -25,26 +464,175 @@ int main ()
// Load a texture from the resources directory // Load a texture from the resources directory
Texture wabbit = LoadTexture("wabbit_alpha.png"); Texture wabbit = LoadTexture("wabbit_alpha.png");
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 // game loop
while (!WindowShouldClose()) // run the loop until the user presses ESCAPE or presses the Close button on the window 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 // drawing
BeginDrawing(); BeginDrawing();
// Setup the back buffer for drawing (clear color and depth buffers) // Setup the back buffer for drawing (clear color and depth buffers)
ClearBackground(BLACK); ClearBackground(BLACK);
// draw some text using the default font // Draw ground
DrawText("Hello Raylib", 200,200,20,WHITE); DrawRectangle(0, screenHeight - groundHeight, screenWidth, groundHeight, BROWN);
// draw our texture to the screen // draw targets
DrawTexture(wabbit, 400, 200, WHITE); 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);
// end the frame and get ready for the next one (display frame, poll input, etc...) // end the frame and get ready for the next one (display frame, poll input, etc...)
EndDrawing(); EndDrawing();
} }
// cleanup // 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 // unload our texture so it can be cleaned up
UnloadTexture(wabbit); UnloadTexture(wabbit);