364 lines
14 KiB
C
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;
|
|
}
|