ollama-game/src/main.c
2026-08-31 18:35:28 +02:00

364 lines
14 KiB
C

#include <SDL.h>
#include <stdio.h>
#include <stdbool.h>
#include <string.h>
#include <SDL_ttf.h>
#include <SDL_mixer.h>
#include <stdlib.h>
/*
* Global Constants
* These define the window dimensions and are used for centering graphics.
*/
const int SCREEN_WIDTH = 800;
const int SCREEN_HEIGHT = 600;
/*
* Player Structure
* Even if invisible, we keep the player state to allow for future gameplay logic.
*/
typedef struct {
float x, y;
float speed;
int size;
SDL_Color color;
} Player;
/*
* Game State Structure
* This is the 'Brain' of the game, containing everything needed to track the current session.
*/
typedef struct {
bool is_running; // Controls the main game loop
// Message 1: "Hello, human."
const char* message1;
int visible_chars1; // Number of characters revealed in message1
uint32_t last_char_time1; // Timestamp of last character revealed in message1
// Message 2: "It is a known fact that C, is superior to every other programming language."
const char* message2;
int visible_chars2; // Number of characters revealed in message2
uint32_t last_char_time2; // Timestamp of last character revealed in message2
int message_stage; // 0: showing message1, 1: showing message2
uint32_t message1_finish_time; // Timestamp when message1 finished revealing (0 if not finished)
// Escape key handling
uint32_t escape_press_time; // Timestamp when Escape was first pressed (0 if not pressed)
// Fonts
TTF_Font* font_large; // Font for message1 (120pt)
TTF_Font* font_small; // Font for message2 (24pt)
TTF_Font* current_font; // Currently used font (points to either font_large or font_small)
// Audio
Mix_Chunk* beep_chunk; // Sound effect for each typed character
// Screen shake effect
float shake_timer; // Time remaining for shake effect (ms)
float shake_duration; // Total duration of shake effect when triggered (ms)
float shake_intensity; // Current intensity of shake (pixels)
Player player; // The player object
} Game;
/**
* Initializes SDL, TTF, and the game state.
* Returns true if everything started correctly, false otherwise.
*/
bool init_game(Game *game) {
// Initialize SDL Video subsystem
if (SDL_Init(SDL_INIT_VIDEO) < 0) {
printf("SDL could not initialize! SDL_Error: %s\n", SDL_GetError());
return false;
}
// Initialize SDL_ttf for text rendering
if (TTF_Init() == -1) {
printf("SDL_ttf could not initialize! TTF_Error: %s\n", TTF_GetError());
return false;
}
// Initialize SDL_mixer for audio with lower frequency (22050 Hz) to lower pitch of beep
if (Mix_OpenAudio(22050, MIX_DEFAULT_FORMAT, 2, 2048) < 0) {
printf("SDL_mixer could not open audio! Mix_Error: %s\n", Mix_GetError());
return false;
}
// Load the fonts
game->font_large = TTF_OpenFont("assets/font.ttf", 120);
if (!game->font_large) {
printf("CRITICAL: Failed to load large font! TTF_Error: %s\n", TTF_GetError());
return false;
}
game->font_small = TTF_OpenFont("assets/font.ttf", 24);
if (!game->font_small) {
printf("CRITICAL: Failed to load small font! TTF_Error: %s\n", TTF_GetError());
return false;
}
game->current_font = game->font_large; // Start with large font for first message
// Load the beep sound effect
game->beep_chunk = Mix_LoadWAV("assets/dialog-beep.wav");
if (!game->beep_chunk) {
printf("WARNING: Failed to load beep sound! Mix_Error: %s\n", Mix_GetError());
// Not critical, continue without sound
}
// Setup initial state
game->is_running = true;
game->escape_press_time = 0; // Not pressing escape initially
game->message1 = "Hello, human.";
game->visible_chars1 = 0;
game->last_char_time1 = SDL_GetTicks();
game->message2 = "This is the remake of the popular game\nUndertale, but rewritten and opensourced in\nthe best programming language known\nto man, C. Enjoy this peak of a GPL3.0 game :3";
game->visible_chars2 = 0;
game->last_char_time2 = 0; // Not used until we switch to message2
game->message_stage = 0; // Start with message1
game->message1_finish_time = 0; // Not finished yet
// Initialize shake effect
game->shake_timer = 0.0f;
game->shake_duration = 300.0f; // 300ms shake duration
game->shake_intensity = 0.0f;
// Seed random number generator for shake effect
srand((unsigned int)SDL_GetTicks());
game->player = (Player){ .x = 100, .y = 100, .speed = 0.5f, .size = 32, .color = {255, 0, 0, 255} };
return true;
}
/**
* Processes keyboard and window events.
* Handles the 'hold to quit' logic for the Escape key.
*/
void handle_input(Game *game, float delta_time) {
SDL_Event event;
while (SDL_PollEvent(&event)) {
if (event.type == SDL_QUIT) {
game->is_running = false;
}
}
const uint8_t *state = SDL_GetKeyboardState(NULL);
// ESCAPE KEY LOGIC:
if (state[SDL_SCANCODE_ESCAPE]) {
if (game->escape_press_time == 0) {
// Just pressed
game->escape_press_time = SDL_GetTicks();
} else if (SDL_GetTicks() - game->escape_press_time >= 1000) {
// Held for 1 second
game->is_running = false;
}
} else {
// Released
game->escape_press_time = 0;
}
// Player Movement (Invisible for now)
if (state[SDL_SCANCODE_UP]) game->player.y -= game->player.speed;
if (state[SDL_SCANCODE_DOWN]) game->player.y += game->player.speed;
if (state[SDL_SCANCODE_LEFT]) game->player.x -= game->player.speed;
if (state[SDL_SCANCODE_RIGHT]) game->player.x += game->player.speed;
}
/**
* Updates game logic independent of rendering.
* Handles the typewriter timing.
*/
void update(Game *game) {
// UPDATE SHAKE EFFECT:
// Decrease shake timer and calculate current intensity
if (game->shake_timer > 0.0f) {
game->shake_timer -= 16.67f; // Approximate 60 FPS frame time
if (game->shake_timer < 0.0f) game->shake_timer = 0.0f;
// Calculate intensity based on remaining timer (linear fade out)
game->shake_intensity = (game->shake_timer / game->shake_duration) * 4.0f; // Max 4px shake
} else {
game->shake_intensity = 0.0f;
}
// TYPEWRITER LOGIC:
uint32_t current_time = SDL_GetTicks();
if (game->message_stage == 0) {
// Handling message1: "Hello, human."
if (current_time - game->last_char_time1 >= 333) { // Adjusted timing
if (game->visible_chars1 < (int)strlen(game->message1)) {
char current_char = game->message1[game->visible_chars1];
game->visible_chars1++;
// Play beep for each character except spaces (already lower pitch due to mixer frequency)
if (game->beep_chunk && current_char != ' ') {
Mix_PlayChannel(-1, game->beep_chunk, 0);
// Trigger shake effect for non-space characters
game->shake_timer = game->shake_duration;
}
game->last_char_time1 = current_time;
}
// Check if we just finished message1 (transition from not finished to finished)
if (game->visible_chars1 == (int)strlen(game->message1) &&
game->message1_finish_time == 0) {
game->message1_finish_time = current_time;
}
}
} else if (game->message_stage == 1) {
// Handling message2: "It is a known fact that C, is superior to every other programming language."
if (current_time - game->last_char_time2 >= 36) { // 1.66x faster timing for second message
if (game->visible_chars2 < (int)strlen(game->message2)) {
char current_char = game->message2[game->visible_chars2];
game->visible_chars2++;
// Play beep for each character except spaces (already lower pitch due to mixer frequency)
if (game->beep_chunk && current_char != ' ') {
Mix_PlayChannel(-1, game->beep_chunk, 0);
// Trigger shake effect for non-space characters
game->shake_timer = game->shake_duration;
}
game->last_char_time2 = current_time;
}
}
}
// Check if we should switch to message2 (after 3.5 seconds of finishing message1)
// This check happens regardless of which message we're currently displaying
if (game->message_stage == 0 && game->message1_finish_time != 0 &&
(current_time - game->message1_finish_time >= 3500)) {
game->message_stage = 1;
game->visible_chars2 = 0;
game->last_char_time2 = current_time;
game->current_font = game->font_small; // Switch to small font
}
// Keep player within screen bounds
if (game->player.x < 0) game->player.x = 0;
if (game->player.x > SCREEN_WIDTH - game->player.size) game->player.x = SCREEN_WIDTH - game->player.size;
if (game->player.y < 0) game->player.y = 0;
if (game->player.y > SCREEN_HEIGHT - game->player.size) game->player.y = SCREEN_HEIGHT - game->player.size;
}
/**
* Handles all drawing operations.
* Clears screen and renders centered text.
*/
void render(SDL_Renderer *renderer, Game *game) {
// 1. Clear the screen to black
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 255);
SDL_RenderClear(renderer);
// 2. Render the revealed portion of the text
if (game->current_font) {
// Determine which message to render based on message_stage
const char* message_to_render;
int visible_chars_to_render;
if (game->message_stage == 0) {
message_to_render = game->message1;
visible_chars_to_render = game->visible_chars1;
} else {
message_to_render = game->message2;
visible_chars_to_render = game->visible_chars2;
}
if (visible_chars_to_render > 0) {
SDL_Color white = {255, 255, 255, 255};
int line_height = TTF_FontLineSkip(game->current_font);
int line_count = 0;
// First pass: count lines to compute total height
int i = 0;
while (i < visible_chars_to_render) {
const char* line_start = message_to_render + i;
// Find end of line (newline or end of visible chars)
int line_len = 0;
while (i + line_len < visible_chars_to_render && message_to_render[i + line_len] != '\n') {
line_len++;
}
line_count++;
i += line_len;
if (i < visible_chars_to_render && message_to_render[i] == '\n') {
i++; // skip newline
}
}
// If no newline encountered, line_count is at least 1
if (line_count == 0) line_count = 1;
int total_height = line_count * line_height;
int start_y = (SCREEN_HEIGHT - total_height) / 2;
// Second pass: render each line
i = 0;
int line_y = 0;
while (i < visible_chars_to_render) {
const char* line_start = message_to_render + i;
int line_len = 0;
while (i + line_len < visible_chars_to_render && message_to_render[i + line_len] != '\n') {
line_len++;
}
// Create temporary buffer for this line
char line_buf[256] = {0};
strncpy(line_buf, line_start, line_len);
line_buf[line_len] = '\0';
SDL_Surface* surface = TTF_RenderText_Blended(game->current_font, line_buf, white);
if (surface) {
SDL_Texture* texture = SDL_CreateTextureFromSurface(renderer, surface);
int shake_offset_x = (rand() % (int)(game->shake_intensity * 2 + 1)) - game->shake_intensity;
int shake_offset_y = (rand() % (int)(game->shake_intensity * 2 + 1)) - game->shake_intensity;
SDL_Rect text_rect = {
(SCREEN_WIDTH - surface->w) / 2 + shake_offset_x,
start_y + line_y + shake_offset_y,
surface->w,
surface->h
};
SDL_RenderCopy(renderer, texture, NULL, &text_rect);
SDL_FreeSurface(surface);
SDL_DestroyTexture(texture);
}
line_y += line_height;
i += line_len;
if (i < visible_chars_to_render && message_to_render[i] == '\n') {
i++; // skip newline
}
}
}
}
// 3. Swap the backbuffer to the screen
SDL_RenderPresent(renderer);
}
int main(int argc, char* args[]) {
Game game;
if (!init_game(&game)) return 1;
SDL_Window *window = SDL_CreateWindow("C Game Template", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, SCREEN_WIDTH, SCREEN_HEIGHT, SDL_WINDOW_SHOWN);
SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
// FPS Locking variables
const int TARGET_FPS = 60;
const uint32_t FRAME_DELAY = 1000 / TARGET_FPS;
while (game.is_running) {
uint32_t frame_start = SDL_GetTicks();
// Calculate Delta Time (time since last frame) in milliseconds
// Since we lock to 60FPS, this is usually around 16.6ms.
float delta_time = 1000.0f / TARGET_FPS;
handle_input(&game, delta_time);
update(&game);
render(renderer, &game);
// FRAME CAPPER:
// Calculate how long the frame took and sleep the remaining time.
uint32_t frame_time = SDL_GetTicks() - frame_start;
if (FRAME_DELAY > frame_time) {
SDL_Delay(FRAME_DELAY - frame_time);
}
}
// Cleanup memory
if (game.font_large) TTF_CloseFont(game.font_large);
if (game.font_small) TTF_CloseFont(game.font_small);
if (game.beep_chunk) Mix_FreeChunk(game.beep_chunk);
Mix_CloseAudio();
Mix_Quit();
SDL_DestroyRenderer(renderer);
SDL_DestroyWindow(window);
TTF_Quit();
SDL_Quit();
return 0;
}