#include #include #include #include #include #include #include /* * 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; }