#include "raylib.h" #include "raymath.h" #include #include #define STAR_COUNT 200 #define PLAYER_MAX_HP 100.0f #define ENEMY_MAX_HP 100.0f #define SWORD_DAMAGE 25.0f #define SWORD_RANGE 5.0f typedef struct { int x; int y; float brightness; float phase; } Star; typedef enum { PAUSE_NONE, PAUSE_RESUME, PAUSE_QUIT } PauseAction; typedef struct { bool swinging; float time; float duration; bool damage_dealt; /* Prevents multi-hit per swing. */ } Sword; typedef enum { STATE_TITLE, STATE_PLAYING } GameState; /* --- INIT --- */ static void InitStars(Star *stars, int count, int width, int height) { for (int i = 0; i < count; i++) { stars[i].x = GetRandomValue(0, width); stars[i].y = GetRandomValue(0, height * 2 / 3); /* Upper sky. */ stars[i].brightness = (float)GetRandomValue(30, 100) / 100.0f; stars[i].phase = (float)GetRandomValue(0, 628) / 100.0f; /* ~0..2pi */ } } /* --- DRAW: 2D BACKGROUND --- */ static void DrawSky(int width, int height) { /* Two vertical gradients: white->blue (top), blue->black (bottom). Replaces 480 DrawLine calls — fixes the tearing. */ Color white = { 255, 255, 255, 255 }; Color blue = { 40, 80, 160, 255 }; Color black = { 0, 0, 0, 255 }; DrawRectangleGradientV(0, 0, width, height / 2, white, blue); DrawRectangleGradientV(0, height / 2, width, height - height / 2, blue, black); } static void DrawStars(const Star *stars, int count, float time) { for (int i = 0; i < count; i++) { float twinkle = 0.5f + 0.5f * sinf(time * 2.0f + stars[i].phase); unsigned char a = (unsigned char)(255.0f * stars[i].brightness * twinkle); DrawCircle(stars[i].x, stars[i].y, 2.0f * twinkle, (Color){ 255, 255, 255, (unsigned char)(a / 3) }); DrawPixel(stars[i].x, stars[i].y, (Color){ 255, 255, 255, a }); } } static void DrawCrosshair(int width, int height) { int cx = width / 2; int cy = height / 2; Color c = { 255, 255, 255, 120 }; DrawLine(cx - 8, cy, cx - 3, cy, c); DrawLine(cx + 3, cy, cx + 8, cy, c); DrawLine(cx, cy - 8, cx, cy - 3, c); DrawLine(cx, cy + 3, cx, cy + 8, c); } static void DrawHealthBar(int x, int y, int w, int h, float hp, float max_hp, Color color) { /* Background. */ DrawRectangle(x, y, w, h, (Color){ 30, 30, 30, 200 }); /* Fill proportional to HP (clamped 0..1). */ float ratio = hp / max_hp; if (ratio < 0.0f) ratio = 0.0f; if (ratio > 1.0f) ratio = 1.0f; DrawRectangle(x, y, (int)(w * ratio), h, color); /* Border. */ DrawRectangleLines(x, y, w, h, RAYWHITE); } /* --- DRAW: PAUSE MENU --- */ static PauseAction DrawPauseMenu(int width, int height) { /* Dim overlay. */ DrawRectangle(0, 0, width, height, (Color){ 0, 0, 0, 160 }); /* Title. */ const char *title = "PAUSED"; int title_size = 60; int title_w = MeasureText(title, title_size); DrawText(title, (width - title_w) / 2, height / 2 - 130, title_size, RAYWHITE); /* Resume button. */ Rectangle resume_btn = { width / 2 - 100, height / 2 - 20, 200, 50 }; bool resume_hover = CheckCollisionPointRec(GetMousePosition(), resume_btn); DrawRectangleRec(resume_btn, resume_hover ? (Color){ 80, 80, 130, 255 } : (Color){ 50, 50, 90, 255 }); DrawRectangleLinesEx(resume_btn, 2, RAYWHITE); DrawText("RESUME", resume_btn.x + 53, resume_btn.y + 12, 30, RAYWHITE); /* Quit button. */ Rectangle quit_btn = { width / 2 - 100, height / 2 + 50, 200, 50 }; bool quit_hover = CheckCollisionPointRec(GetMousePosition(), quit_btn); DrawRectangleRec(quit_btn, quit_hover ? (Color){ 130, 50, 50, 255 } : (Color){ 90, 30, 30, 255 }); DrawRectangleLinesEx(quit_btn, 2, RAYWHITE); DrawText("QUIT", quit_btn.x + 68, quit_btn.y + 12, 30, RAYWHITE); /* Handle clicks. */ if (resume_hover && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) return PAUSE_RESUME; if (quit_hover && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) return PAUSE_QUIT; return PAUSE_NONE; } /* --- DRAW: TITLE SCREEN --- */ static bool DrawTitleScreen(int width, int height, float alpha) { /* Title text. */ const char *title = "prj tiny"; int title_size = 80; int title_w = MeasureText(title, title_size); DrawText(title, (width - title_w) / 2, height / 2 - 120, title_size, (Color){ 255, 255, 255, (unsigned char)(255 * alpha) }); /* Subtitle. */ const char *subtitle = "A raylib project"; int sub_w = MeasureText(subtitle, 20); DrawText(subtitle, (width - sub_w) / 2, height / 2 - 40, 20, (Color){ 200, 200, 200, (unsigned char)(200 * alpha) }); /* Play button. */ Rectangle play_btn = { width / 2 - 80, height / 2 + 20, 160, 50 }; bool play_hover = CheckCollisionPointRec(GetMousePosition(), play_btn); unsigned char a = (unsigned char)(255 * alpha); DrawRectangleRec(play_btn, play_hover ? (Color){ 80, 80, 130, a } : (Color){ 50, 50, 90, a }); DrawRectangleLinesEx(play_btn, 2, (Color){ 255, 255, 255, a }); DrawText("PLAY", play_btn.x + 50, play_btn.y + 12, 30, (Color){255, 255, 255, a }); /* Hint. */ const char *hint = "Click PLAY or press ENTER"; int hint_w = MeasureText(hint, 18); DrawText(hint, (width - hint_w) / 2, height - 60, 18, (Color){ 150, 150, 150, (unsigned char)(180 * alpha) }); if (play_hover && IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) return true; return false; } /* --- DRAW: SWORD --- */ static void DrawSword(const Camera3D *camera, const Sword *sword) { /* Camera basis vectors (forward, right, up). */ Vector3 fwd = Vector3Normalize(Vector3Subtract(camera->target, camera->position)); Vector3 right = Vector3Normalize(Vector3CrossProduct(fwd, (Vector3){ 0.0f, 1.0f, 0.0f })); Vector3 up = Vector3CrossProduct(right, fwd); /* Hand position: forward, to the right, and below the camera. */ Vector3 hand = camera->position; hand = Vector3Add(hand, Vector3Scale(fwd, 2.0f)); hand = Vector3Add(hand, Vector3Scale(right, 1.1f)); hand = Vector3Add(hand, Vector3Scale(up, -1.0f)); /* Swing progress 0..1, eased (fast strike, slow recovery). */ float t = 0.0f; if (sword->swinging) { t = sword->time / sword->duration; if (t > 1.0f) t = 1.0f; } float ease = 1.0f - (1.0f - t) * (1.0f - t); /* easeOutQuad. */ /* Blade direction via pitch (up/down) and yaw (left/right) */ float pitch; float yaw; if (sword->swinging) { pitch = Lerp(1.0f, -0.6f, ease); /* raised -> lowered */ yaw = Lerp(0.9f, -0.7f, ease); /* right -> left */ } else { pitch = -0.25f; yaw = 0.15f; } /* Direction in camera-local space, then transformed to world. */ Vector3 dir_local = { sinf(yaw) * cosf(pitch), sinf(pitch), cosf(yaw) * cosf(pitch) }; Vector3 dir_world = Vector3Add( Vector3Add(Vector3Scale(right, dir_local.x), Vector3Scale(up, dir_local.y)), Vector3Scale(fwd, dir_local.z)); float blade_len = 2.2f; Vector3 tip = Vector3Add(hand, Vector3Scale(dir_world, blade_len)); /* Handle: short stub behind the hand. */ Vector3 handle_end = Vector3Subtract(hand, Vector3Scale(dir_world, 0.5f)); DrawCylinderEx(handle_end, hand, 0.07f, 0.07f, 6, BROWN); /* Guard: small block at the hand. */ DrawCube(hand, 0.35f, 0.12f, 0.12f, DARKGRAY); /* Blade: tapered cylinder from hand to tip. */ DrawCylinderEx(hand, tip, 0.06f, 0.01f, 6, (Color){ 180, 190, 200, 255 }); /* Tip cap. */ DrawSphere(tip, 0.04f, (Color){ 220, 225, 235, 255 }); } static void DrawEnemySword(Vector3 origin, Vector3 face_dir, const Sword *sword) { /* Basis vectors from the enemy's facing direction. */ Vector3 fwd = Vector3Normalize(face_dir); Vector3 right = Vector3Normalize(Vector3CrossProduct(fwd, (Vector3){ 0.0f, 1.0f, 0.0f })); Vector3 up = { 0.0f, 1.0f, 0.0f }; /* Hand: to the enemy's right, slightly forward and up. */ Vector3 hand = origin; hand = Vector3Add(hand, Vector3Scale(right, 1.3f)); hand = Vector3Add(hand, Vector3Scale(fwd, 0.5f)); hand = Vector3Add(hand, Vector3Scale(up, 0.5f)); /* Swing progress 0..1, eased (same as player sword). */ float t = 0.0f; if (sword->swinging) { t = sword->time / sword -> duration; if (t > 1.0f) t = 1.0f; } float ease = 1.0f - (1.0f - t) * (1.0f - t); float pitch; float yaw; if (sword->swinging) { pitch = Lerp(1.0f, 0.6f, ease); yaw = Lerp(0.9f, -0.7f, ease); } else { pitch = -0.25f; yaw = 0.15f; } Vector3 dir_local = { sinf(yaw) * cosf(pitch), sinf(pitch), cosf(yaw) * cosf(pitch) }; Vector3 dir_world = Vector3Add( Vector3Add(Vector3Scale(right, dir_local.x), Vector3Scale(up, dir_local.y)), Vector3Scale(fwd, dir_local.z)); float blade_len = 2.2f; Vector3 tip = Vector3Add(hand, Vector3Scale(dir_world, blade_len)); Vector3 handle_end = Vector3Subtract(hand, Vector3Scale(dir_world, 0.5f)); DrawCylinderEx(handle_end, hand, 0.07f, 0.07f, 6, BROWN); DrawCube(hand, 0.35f, 0.12f, 0.12f, DARKGRAY); /* Darker blade tint to distinguish from the player's sword. */ DrawCylinderEx(hand, tip, 0.06f, 0.01f, 6, (Color){ 120, 125, 135, 255 }); DrawSphere(tip, 0.04f, (Color){ 150, 155, 165, 255 }); } /* --- SOUND GENERATION (procedural, no files needed) --- */ static Sound MakeSwordSwingSound(void) { /* Whoosh: noise burst + frequency sweep, fast decay. */ int sampleRate = 44100; int sampleCount = (int)(sampleRate * 0.25f); short *data = malloc(sampleCount * sizeof(short)); for (int i = 0; i < sampleCount; i++) { float t = (float)i / (float)sampleCount; float env = expf(-t * 7.0f); float noise = ((float)rand() / (float)RAND_MAX * 2.0f - 1.0f); float sweep = sinf(t * 1200.0f) * (1.0f - t); float sample = (noise * 0.6f + sweep * 0.4f) * env; data[i] = (short)(sample * 28000.0f); } Wave wave = { 0 }; wave.frameCount = sampleCount; wave.sampleRate = sampleRate; wave.sampleSize = 16; wave.channels = 1; wave.data = data; Sound snd = LoadSoundFromWave(wave); free(data); return snd; } static Sound MakeFootstepSound(void) { /* Short thud: low sine + noise, very fast decay. */ int sampleRate = 44100; int sampleCount = (int)(sampleRate * 0.09f); short *data = malloc(sampleCount * sizeof(short)); for (int i = 0; i < sampleCount; i++) { float t = (float)i / (float)sampleCount; float env = expf(-t * 22.0f); float noise = ((float)rand() / (float)RAND_MAX * 2.0f - 1.0f); float thud = sinf(t * 70.0f) * 0.5f; float sample = (noise * 0.5f + thud) * env * 0.5f; data[i] = (short)(sample * 28000.0f); } Wave wave = { 0 }; wave.frameCount = sampleCount; wave.sampleRate = sampleRate; wave.sampleSize = 16; wave.channels = 1; wave.data = data; Sound snd = LoadSoundFromWave(wave); free(data); return snd; } static Sound MakeAmbienceSound(void) { /* Low drone: two sine layers + faint wind noise. Loops. */ int sampleRate = 44100; int sampleCount = (int)(sampleRate * 3.0f); short *data = malloc(sampleCount * sizeof(short)); for (int i = 0; i < sampleCount; i++) { float t = (float)i / (float)sampleRate; float drone = sinf(t * 2.0f * 3.14159265f * 55.0f) * 0.3f; drone += sinf(t * 2.0f * 3.14159265f * 82.5f) * 0.15f; float wind = ((float)rand() / (float)RAND_MAX * 2.0f - 1.0f) * 0.08f; float sample = drone + wind; data[i] = (short)(sample * 20000.0f); } Wave wave = { 0 }; wave.frameCount = sampleCount; wave.sampleRate = sampleRate; wave.sampleSize = 16; wave.channels = 1; wave.data = data; Sound snd = LoadSoundFromWave(wave); free(data); return snd; } /* --- MAIN --- */ int main(int argc, char *argv[]) { (void)argc; (void)argv; const int screen_width = 720; const int screen_height = 480; InitWindow(screen_width, screen_height, "prj tiny"); SetTargetFPS(60); SetExitKey(KEY_NULL); /* Take over ESC ourselves. */ InitAudioDevice(); Sound snd_swing = MakeSwordSwingSound(); Sound snd_footstep = MakeFootstepSound(); Sound snd_ambience = MakeAmbienceSound(); SetSoundVolume(snd_swing, 0.5f); SetSoundVolume(snd_footstep, 0.35f); SetSoundVolume(snd_ambience, 0.3f); /* Background music (your own .ogg file). */ Music music = LoadMusicStream("mus_example.ogg"); SetMusicVolume(music, 1.0f); PlayMusicStream(music); /* 3D Camera. */ Camera3D camera = { 0 }; camera.position = (Vector3){ 0.0f, 2.0f, 8.0f }; camera.target = (Vector3){ 0.0f, 1.0f, 0.0f }; camera.up = (Vector3){ 0.0f, 1.0f, 0.0f }; camera.fovy = 60.0f; camera.projection = CAMERA_PERSPECTIVE; Star stars[STAR_COUNT]; InitStars(stars, STAR_COUNT, screen_width, screen_height); bool paused = false; Sword sword = { false, 0.0f, 0.35f, false }; Sword enemy_sword = { false, 0.0f, 0.35f, false }; float enemy_swing_timer = 3.0f; /* Seconds until next enemy swing. */ float player_hp = PLAYER_MAX_HP; float enemy_hp = ENEMY_MAX_HP; float death_timer = 0.0f; /* Counts down respawn delay. */ float end_fade = 0.0f; /* Fade-to-black after enemy death (KH-style). */ bool game_ending = false; /* True once enemy slain; game fades out. */ GameState game_state = STATE_TITLE; float title_alpha = 0.0f; bool title_exiting = false; Vector3 prev_pos = camera.position; float footstep_timer = 0.0f; EnableCursor(); /* Cursor visible on title screen for clicking. */ while (!WindowShouldClose()) { float dt = GetFrameTime(); if (game_state == STATE_TITLE) { /* Fade in on enter, fade out when exiting. */ if (title_exiting) { title_alpha -= dt * 3.0f; if (title_alpha <= 0.0f) { title_alpha = 0.0f; game_state = STATE_PLAYING; DisableCursor(); } } else { title_alpha += dt * 2.0f; /* ~0.5s fade in. */ if (title_alpha > 1.0f) title_alpha = 1.0f; } } /* --- INPUT --- */ if (game_state == STATE_PLAYING && IsKeyPressed(KEY_ESCAPE)) { paused = !paused; if (paused) EnableCursor(); else DisableCursor(); } if (game_state == STATE_PLAYING && !paused) UpdateCamera(&camera, CAMERA_FIRST_PERSON); /* Footsteps: detect horizontal movement, play at intervals. */ if (game_state == STATE_PLAYING && !paused && player_hp > 0.0f) { float moved = Vector3Distance(camera.position, prev_pos); if (moved > 0.001f) { footstep_timer -= dt; if (footstep_timer <= 0.0f) { PlaySound(snd_footstep); footstep_timer = 0.35f; } } else footstep_timer = 0.0f; prev_pos = camera.position; } /* Sword swing: left-click to strike, frame-time timer. */ if (game_state == STATE_PLAYING && !paused && IsMouseButtonPressed(MOUSE_BUTTON_LEFT) && !sword.swinging && player_hp > 0.0f) { sword.swinging= true; sword.time = 0.0f; sword.damage_dealt = false; PlaySound(snd_swing); } if (sword.swinging) { sword.time += GetFrameTime(); /* Deal damage at swing midpoint if enemy is in range. */ float t = sword.time / sword.duration; if (t >= 0.4f && !sword.damage_dealt) { sword.damage_dealt = true; float dist = Vector3Distance(camera.position, (Vector3){ 0.0f, 1.05f, 0.0f }); if (dist <= SWORD_RANGE && enemy_hp > 0.0f) enemy_hp -= SWORD_DAMAGE; } if (sword.time >= sword.duration) sword.swinging = false; } /* Enemy sword: auto-swings on a random timer (2-4 seconds). */ if (game_state == STATE_PLAYING && !paused) { if (!enemy_sword.swinging) { enemy_swing_timer -= GetFrameTime(); if (enemy_swing_timer <= 0.0f && enemy_hp > 0.0f) { enemy_sword.swinging = true; enemy_sword.time = 0.0f; enemy_sword.damage_dealt = false; SetSoundPitch(snd_swing, 0.8f); PlaySound(snd_swing); SetSoundPitch(snd_swing, 1.0f); } } else { enemy_sword.time += GetFrameTime(); /* Deal damage to player at swing midpoint if in range. */ float et = enemy_sword.time / enemy_sword.duration; if (et >= 0.4 && !enemy_sword.damage_dealt) { enemy_sword.damage_dealt = true; float dist = Vector3Distance(camera.position, (Vector3){ 0.0f, 1.05f, 0.0f }); if (dist <= SWORD_RANGE && player_hp > 0.0f) player_hp -= SWORD_DAMAGE; } if (enemy_sword.time >= enemy_sword.duration) { enemy_sword.swinging = false; enemy_swing_timer = (float)GetRandomValue(20, 40) / 10.0f; } } } /* --- DEATH / RESPAWN --- */ if (game_state == STATE_PLAYING && !paused) { /* Enemy slain: trigger KH-style fade-to-black ending. */ if (enemy_hp <= 0.0f && !game_ending) game_ending = true; if (game_ending) { end_fade += dt * 0.5f; /* ~2s fade to black. */ if (end_fade >= 1.0f) { end_fade = 1.0f; break; /* End the game. */ } } /* Player death: respawn after a delay. */ if (player_hp <= 0.0f && !game_ending) { death_timer += dt; if (death_timer >= 2.0f) { player_hp = PLAYER_MAX_HP; death_timer = 0.0f; } } } /* Looping ambient drone — replay when it finishes. */ if (!IsSoundPlaying(snd_ambience)) PlaySound(snd_ambience); /* Keep music streaming (must be called every frame). */ UpdateMusicStream(music); /* --- DRAW --- */ float time = (float)GetTime(); BeginDrawing(); ClearBackground(BLACK); DrawSky(screen_width, screen_height); DrawStars(stars, STAR_COUNT, time); if (game_state == STATE_TITLE) { bool play_clicked = DrawTitleScreen(screen_width, screen_height, title_alpha); if ((play_clicked || IsKeyPressed(KEY_ENTER)) && !title_exiting) title_exiting = true; } if (game_state == STATE_PLAYING) { BeginMode3D(camera); DrawPlane((Vector3){ 0, 0, 0 }, (Vector2){ 256, 256 }, BEIGE); DrawCube((Vector3){ 0, 1.05f, 0 }, 2, 2, 2, RED); if (!paused) { DrawSword(&camera, &sword); Vector3 enemy_pos = { 0.0f, 1.05f, 0.0f }; Vector3 face_dir = Vector3Subtract(camera.position, enemy_pos); face_dir.y = 0.0f; /* Stay on the horizontal plane. */ DrawEnemySword(enemy_pos, face_dir, &enemy_sword); } EndMode3D(); if (!paused) { DrawCrosshair(screen_width, screen_height); DrawText("prj tiny - ESC to pause", 10, 10, 20, DARKGRAY); /* Player health bar (top-left). */ DrawHealthBar(10, 40, 200, 20, player_hp, PLAYER_MAX_HP, GREEN); DrawText("PLAYER", 10, 62, 16, RAYWHITE); /* Enemy health bar (top-right). */ DrawHealthBar(screen_width - 210, 40, 200, 20, enemy_hp, ENEMY_MAX_HP, RED); DrawText("ENEMY", screen_width - 210, 62, 16, RAYWHITE); /* Death messages. */ if (player_hp <= 0.0f) DrawText("YOU DIED", screen_width / 2 - 80, screen_height / 2, 40, RED); else if (enemy_hp <= 0.0f) DrawText("ENEMY SLAIN", screen_width / 2 - 110, screen_height / 2, 40, GREEN); } else { PauseAction action = DrawPauseMenu(screen_width, screen_height); if (action == PAUSE_RESUME) { paused = false; DisableCursor(); } else if (action == PAUSE_QUIT) break; } } if (game_ending) DrawRectangle(0, 0, screen_width, screen_height, (Color){ 0, 0, 0, (unsigned char)(255 * end_fade) }); EndDrawing(); } UnloadMusicStream(music); UnloadSound(snd_swing); UnloadSound(snd_footstep); UnloadSound(snd_ambience); CloseAudioDevice(); CloseWindow(); return 0; }