Created
August 1, 2025 12:38
-
-
Save 7etsuo/b50137e20fed9778b4503f23ad1417fa to your computer and use it in GitHub Desktop.
echos.c
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include <math.h> | |
#include <raylib.h> | |
#include <raymath.h> | |
#include <stdio.h> | |
#include <stdlib.h> | |
#include <string.h> | |
#include <time.h> | |
#define SCREEN_WIDTH 1280 | |
#define SCREEN_HEIGHT 720 | |
#define WORLD_SIZE 100 | |
#define CHUNK_SIZE 16 | |
#define MAX_ECHO_ENERGY 10 | |
#define ECHO_PULSE_RADIUS 30.0f | |
#define ECHO_PULSE_DURATION 3.0f | |
#define VISIBILITY_RADIUS 5.0f | |
#define MAX_ENEMIES 50 | |
#define MAX_CRYSTALS 100 | |
#define MAX_ANCHORED_AREAS 100 | |
#define MAX_SOUND_PARTICLES 200 | |
#define MAX_ECHO_BRIDGES 20 | |
// Enumerations | |
typedef enum | |
{ | |
TOOL_NONE, | |
TOOL_GRAVITY, | |
TOOL_TIME_DILATION, | |
TOOL_ECHO_BRIDGE | |
} ResonanceTool; | |
typedef enum | |
{ | |
ENEMY_ANGER, | |
ENEMY_FEAR, | |
ENEMY_CURIOSITY | |
} EnemyType; | |
typedef enum | |
{ | |
VIEW_FIRST_PERSON, | |
VIEW_ISOMETRIC | |
} ViewMode; | |
// Structures | |
typedef struct | |
{ | |
Vector3 position; | |
float radius; | |
float timeLeft; | |
float strength; | |
} EchoPulse; | |
typedef struct | |
{ | |
Vector3 position; | |
bool collected; | |
float glowTimer; | |
} EchoCrystal; | |
typedef struct | |
{ | |
Vector3 position; | |
float radius; | |
bool active; | |
float visibility; | |
} AnchoredArea; | |
typedef struct | |
{ | |
Vector3 position; | |
Vector3 velocity; | |
EnemyType type; | |
bool active; | |
float speed; | |
float detectRadius; | |
float behavior; | |
} Enemy; | |
typedef struct | |
{ | |
float heights[WORLD_SIZE][WORLD_SIZE]; | |
bool revealed[WORLD_SIZE][WORLD_SIZE]; | |
float visibility[WORLD_SIZE][WORLD_SIZE]; | |
bool anchored[WORLD_SIZE][WORLD_SIZE]; | |
float memory[WORLD_SIZE][WORLD_SIZE]; | |
} World; | |
typedef struct | |
{ | |
Vector3 position; | |
float slowFactor; | |
float radius; | |
float timeLeft; | |
} TimeDilationZone; | |
typedef struct | |
{ | |
Vector3 start; | |
Vector3 end; | |
bool active; | |
float width; | |
} EchoBridge; | |
typedef struct | |
{ | |
Vector3 position; | |
Vector3 velocity; | |
float life; | |
Color color; | |
} SoundParticle; | |
typedef struct | |
{ | |
int aggressiveActions; | |
int passiveActions; | |
float noiseLevel; | |
float lastActionTime; | |
} PlayerBehavior; | |
// Global variables | |
typedef struct | |
{ | |
// Core systems | |
World world; | |
Camera camera; | |
Camera3D isometricCamera; | |
ViewMode cameraMode; | |
Vector3 playerPosition; | |
float playerRotation; | |
// Echo system | |
int echoEnergy; | |
EchoPulse currentPulse; | |
bool pulsing; | |
float pulseTimer; | |
float cameraTransitionTimer; | |
// Game objects | |
EchoCrystal crystals[MAX_CRYSTALS]; | |
Enemy enemies[MAX_ENEMIES]; | |
AnchoredArea anchoredAreas[MAX_ANCHORED_AREAS]; | |
int anchoredCount; | |
// Tools | |
ResonanceTool selectedTool; | |
TimeDilationZone timeDilationZones[10]; | |
EchoBridge echoBridges[MAX_ECHO_BRIDGES]; | |
int bridgeCount; | |
// Effects | |
SoundParticle soundParticles[MAX_SOUND_PARTICLES]; | |
// Player behavior tracking | |
PlayerBehavior behavior; | |
// Game state | |
bool gameOver; | |
float deltaTime; | |
} GameState; | |
// Function prototypes | |
void InitGame (GameState *game); | |
void UpdateGame (GameState *game); | |
void DrawGame (GameState *game); | |
void GenerateWorld (GameState *game); | |
void UpdatePlayerMovement (GameState *game); | |
void UpdateCameraSystem (GameState *game); | |
void HandleEchoPulse (GameState *game); | |
void UpdateEchoPulse (GameState *game); | |
void DrawWorld (GameState *game); | |
void DrawEchoPulseEffect (GameState *game); | |
void UpdateWorldVisibility (GameState *game); | |
void HandleWorldAnchoring (GameState *game); | |
void UpdateEnemies (GameState *game); | |
void SpawnEnemy (GameState *game, EnemyType type); | |
void UpdateCrystals (GameState *game); | |
void DrawCrystals (GameState *game); | |
void DrawEnemies (GameState *game); | |
void HandleResonanceTools (GameState *game); | |
void UpdateTimeDilationZones (GameState *game); | |
void DrawTimeDilationZones (GameState *game); | |
void DrawEchoBridges (GameState *game); | |
void UpdateSoundParticles (GameState *game); | |
void DrawSoundParticles (GameState *game); | |
void CreateSoundParticle (GameState *game, Vector3 position, Color color); | |
void UpdateWorldMemory (GameState *game); | |
void DrawUI (GameState *game); | |
float GetTerrainHeight (GameState *game, float x, float z); | |
bool IsPositionRevealed (GameState *game, float x, float z); | |
bool IsPositionAnchored (GameState *game, float x, float z); | |
void ApplyGravityManipulation (GameState *game, Vector3 position, | |
float strength); | |
void CreateTimeDilationZone (GameState *game, Vector3 position); | |
void CreateEchoBridge (GameState *game, Vector3 start, Vector3 end); | |
// Main function | |
int | |
main (void) | |
{ | |
// WSL2 compatibility - disable MSAA and vsync | |
SetConfigFlags (FLAG_MSAA_4X_HINT | FLAG_VSYNC_HINT); | |
InitWindow (1280, 720, "Echoes of the Unseen"); | |
// Disable VSync for software renderer | |
SetTargetFPS (60); | |
GameState game = { 0 }; | |
InitGame (&game); | |
// WSL window visibility hack - draw a few frames to ensure window appears | |
for (int i = 0; i < 3; i++) | |
{ | |
BeginDrawing (); | |
ClearBackground (BLACK); | |
DrawText ("Loading...", SCREEN_WIDTH / 2 - 50, SCREEN_HEIGHT / 2, 20, | |
WHITE); | |
EndDrawing (); | |
} | |
// Main game loop | |
int frameCount = 0; | |
while (!WindowShouldClose () && !game.gameOver) | |
{ | |
UpdateGame (&game); | |
DrawGame (&game); | |
// Debug output every 60 frames (1 second at 60 FPS) | |
frameCount++; | |
if (frameCount % 60 == 0) | |
{ | |
printf ("Frame %d - Player pos: %.2f, %.2f, %.2f\n", frameCount, | |
game.playerPosition.x, game.playerPosition.y, | |
game.playerPosition.z); | |
} | |
} | |
CloseWindow (); | |
return 0; | |
} | |
// Initialize game | |
void | |
InitGame (GameState *game) | |
{ | |
srand (time (NULL)); | |
// Initialize player | |
game->playerPosition | |
= (Vector3){ WORLD_SIZE / 2.0f, 2.0f, WORLD_SIZE / 2.0f }; | |
game->playerRotation = 0.0f; | |
game->echoEnergy = MAX_ECHO_ENERGY; | |
game->selectedTool = TOOL_NONE; | |
// Initialize cameras | |
game->cameraMode = VIEW_FIRST_PERSON; | |
game->camera.position = game->playerPosition; | |
game->camera.target = Vector3Add (game->playerPosition, (Vector3){ 0, 0, 1 }); | |
game->camera.up = (Vector3){ 0, 1, 0 }; | |
game->camera.fovy = 60.0f; | |
game->camera.projection = CAMERA_PERSPECTIVE; | |
// Isometric camera setup | |
game->isometricCamera.position = (Vector3){ 50, 50, 50 }; | |
game->isometricCamera.target = (Vector3){ WORLD_SIZE / 2, 0, WORLD_SIZE / 2 }; | |
game->isometricCamera.up = (Vector3){ 0, 1, 0 }; | |
game->isometricCamera.fovy = 45.0f; | |
game->isometricCamera.projection = CAMERA_ORTHOGRAPHIC; | |
// Generate world | |
GenerateWorld (game); | |
// Initialize arrays | |
game->anchoredCount = 0; | |
game->bridgeCount = 0; | |
for (int i = 0; i < 10; i++) | |
{ | |
game->timeDilationZones[i].timeLeft = 0; | |
} | |
for (int i = 0; i < MAX_ENEMIES; i++) | |
{ | |
game->enemies[i].active = false; | |
} | |
// Initialize crystals | |
for (int i = 0; i < MAX_CRYSTALS; i++) | |
{ | |
game->crystals[i].position | |
= (Vector3){ GetRandomValue (10, WORLD_SIZE - 10), 0, | |
GetRandomValue (10, WORLD_SIZE - 10) }; | |
game->crystals[i].position.y | |
= GetTerrainHeight (game, game->crystals[i].position.x, | |
game->crystals[i].position.z) | |
+ 0.5f; | |
game->crystals[i].collected = false; | |
game->crystals[i].glowTimer = GetRandomValue (0, 360) * DEG2RAD; | |
} | |
// Initialize visibility around player | |
int px = (int)game->playerPosition.x; | |
int pz = (int)game->playerPosition.z; | |
for (int x = px - VISIBILITY_RADIUS; x <= px + VISIBILITY_RADIUS; x++) | |
{ | |
for (int z = pz - VISIBILITY_RADIUS; z <= pz + VISIBILITY_RADIUS; z++) | |
{ | |
if (x >= 0 && x < WORLD_SIZE && z >= 0 && z < WORLD_SIZE) | |
{ | |
float dist | |
= Vector2Distance ((Vector2){ x, z }, (Vector2){ px, pz }); | |
if (dist <= VISIBILITY_RADIUS) | |
{ | |
game->world.visibility[x][z] | |
= 1.0f - (dist / VISIBILITY_RADIUS); | |
game->world.revealed[x][z] = true; | |
} | |
} | |
} | |
} | |
} | |
// Generate procedural world | |
void | |
GenerateWorld (GameState *game) | |
{ | |
// Simple perlin-noise-like terrain generation | |
for (int x = 0; x < WORLD_SIZE; x++) | |
{ | |
for (int z = 0; z < WORLD_SIZE; z++) | |
{ | |
float height = 0; | |
float amplitude = 5.0f; | |
float frequency = 0.05f; | |
for (int i = 0; i < 4; i++) | |
{ | |
height += amplitude * sinf (x * frequency) * cosf (z * frequency); | |
amplitude *= 0.5f; | |
frequency *= 2.0f; | |
} | |
game->world.heights[x][z] = height; | |
game->world.revealed[x][z] = false; | |
game->world.visibility[x][z] = 0.0f; | |
game->world.anchored[x][z] = false; | |
game->world.memory[x][z] = 0.0f; | |
} | |
} | |
} | |
// Update game logic | |
void | |
UpdateGame (GameState *game) | |
{ | |
game->deltaTime = GetFrameTime (); | |
// Handle window resize | |
if (IsWindowResized ()) | |
{ | |
// Update camera aspect ratio | |
game->camera.fovy = 60.0f; | |
game->isometricCamera.fovy = 45.0f; | |
} | |
UpdatePlayerMovement (game); | |
UpdateCameraSystem (game); | |
HandleEchoPulse (game); | |
UpdateEchoPulse (game); | |
UpdateWorldVisibility (game); | |
HandleWorldAnchoring (game); | |
HandleResonanceTools (game); | |
UpdateEnemies (game); | |
UpdateCrystals (game); | |
UpdateTimeDilationZones (game); | |
UpdateSoundParticles (game); | |
UpdateWorldMemory (game); | |
} | |
// Update player movement | |
void | |
UpdatePlayerMovement (GameState *game) | |
{ | |
// Toggle fullscreen with F11 (WSLg workaround) | |
if (IsKeyPressed (KEY_F11)) | |
{ | |
ToggleFullscreen (); | |
} | |
if (game->cameraMode == VIEW_ISOMETRIC) | |
return; | |
float moveSpeed = 5.0f * game->deltaTime; | |
float rotSpeed = 2.0f * game->deltaTime; | |
// Rotation | |
if (IsKeyDown (KEY_A)) | |
game->playerRotation -= rotSpeed; | |
if (IsKeyDown (KEY_D)) | |
game->playerRotation += rotSpeed; | |
// Movement | |
Vector3 forward | |
= { sinf (game->playerRotation), 0, cosf (game->playerRotation) }; | |
Vector3 right | |
= { cosf (game->playerRotation), 0, -sinf (game->playerRotation) }; | |
Vector3 movement = { 0, 0, 0 }; | |
if (IsKeyDown (KEY_W)) | |
movement = Vector3Add (movement, forward); | |
if (IsKeyDown (KEY_S)) | |
movement = Vector3Subtract (movement, forward); | |
if (IsKeyDown (KEY_Q)) | |
movement = Vector3Subtract (movement, right); | |
if (IsKeyDown (KEY_E)) | |
movement = Vector3Add (movement, right); | |
if (Vector3Length (movement) > 0) | |
{ | |
movement = Vector3Normalize (movement); | |
Vector3 newPos = Vector3Add (game->playerPosition, | |
Vector3Scale (movement, moveSpeed)); | |
// Boundary check | |
if (newPos.x >= 0 && newPos.x < WORLD_SIZE && newPos.z >= 0 | |
&& newPos.z < WORLD_SIZE) | |
{ | |
newPos.y = GetTerrainHeight (game, newPos.x, newPos.z) + 1.5f; | |
game->playerPosition = newPos; | |
} | |
} | |
} | |
// Update camera system | |
void | |
UpdateCameraSystem (GameState *game) | |
{ | |
if (game->cameraTransitionTimer > 0) | |
{ | |
game->cameraTransitionTimer -= game->deltaTime; | |
if (game->cameraTransitionTimer <= 0) | |
{ | |
game->cameraMode = VIEW_FIRST_PERSON; | |
} | |
} | |
if (game->cameraMode == VIEW_FIRST_PERSON) | |
{ | |
game->camera.position = game->playerPosition; | |
game->camera.target = Vector3Add ( | |
game->playerPosition, (Vector3){ sinf (game->playerRotation), 0, | |
cosf (game->playerRotation) }); | |
} | |
else | |
{ | |
// Update isometric camera to follow pulse | |
if (game->pulsing) | |
{ | |
game->isometricCamera.target = game->playerPosition; | |
float camDist = 30.0f + game->currentPulse.radius; | |
game->isometricCamera.position = Vector3Add ( | |
game->playerPosition, (Vector3){ camDist, camDist, camDist }); | |
} | |
} | |
} | |
// Handle echo pulse | |
void | |
HandleEchoPulse (GameState *game) | |
{ | |
if (IsKeyPressed (KEY_SPACE) && game->echoEnergy > 0 && !game->pulsing) | |
{ | |
game->pulsing = true; | |
game->echoEnergy--; | |
game->currentPulse.position = game->playerPosition; | |
game->currentPulse.radius = 0; | |
game->currentPulse.timeLeft = ECHO_PULSE_DURATION; | |
game->currentPulse.strength = 1.0f; | |
game->pulseTimer = 0; | |
game->cameraMode = VIEW_ISOMETRIC; | |
game->cameraTransitionTimer = ECHO_PULSE_DURATION; | |
// Track player behavior | |
game->behavior.noiseLevel += 2.0f; | |
game->behavior.aggressiveActions++; | |
// Create sound particles | |
for (int i = 0; i < 20; i++) | |
{ | |
CreateSoundParticle (game, game->currentPulse.position, SKYBLUE); | |
} | |
} | |
} | |
// Update echo pulse | |
void | |
UpdateEchoPulse (GameState *game) | |
{ | |
if (!game->pulsing) | |
return; | |
game->pulseTimer += game->deltaTime; | |
game->currentPulse.radius = game->pulseTimer * 20.0f; | |
game->currentPulse.timeLeft -= game->deltaTime; | |
game->currentPulse.strength | |
= game->currentPulse.timeLeft / ECHO_PULSE_DURATION; | |
if (game->currentPulse.timeLeft <= 0) | |
{ | |
game->pulsing = false; | |
} | |
// Reveal terrain hit by pulse | |
if (game->pulsing) | |
{ | |
for (int x = 0; x < WORLD_SIZE; x++) | |
{ | |
for (int z = 0; z < WORLD_SIZE; z++) | |
{ | |
float dist = Vector2Distance ( | |
(Vector2){ x, z }, | |
(Vector2){ game->currentPulse.position.x, | |
game->currentPulse.position.z }); | |
if (dist <= game->currentPulse.radius | |
&& dist >= game->currentPulse.radius - 2.0f) | |
{ | |
game->world.revealed[x][z] = true; | |
game->world.visibility[x][z] | |
= fmaxf (game->world.visibility[x][z], | |
game->currentPulse.strength); | |
} | |
} | |
} | |
} | |
} | |
// Update world visibility | |
void | |
UpdateWorldVisibility (GameState *game) | |
{ | |
// Fade visibility over time | |
for (int x = 0; x < WORLD_SIZE; x++) | |
{ | |
for (int z = 0; z < WORLD_SIZE; z++) | |
{ | |
if (!game->world.anchored[x][z] && game->world.visibility[x][z] > 0) | |
{ | |
game->world.visibility[x][z] -= 0.1f * game->deltaTime; | |
if (game->world.visibility[x][z] < 0) | |
{ | |
game->world.visibility[x][z] = 0; | |
} | |
} | |
} | |
} | |
// Maintain visibility around player | |
int px = (int)game->playerPosition.x; | |
int pz = (int)game->playerPosition.z; | |
int radius = (int)VISIBILITY_RADIUS; | |
for (int x = px - radius; x <= px + radius; x++) | |
{ | |
for (int z = pz - radius; z <= pz + radius; z++) | |
{ | |
if (x >= 0 && x < WORLD_SIZE && z >= 0 && z < WORLD_SIZE) | |
{ | |
float dist | |
= Vector2Distance ((Vector2){ x, z }, (Vector2){ px, pz }); | |
if (dist <= VISIBILITY_RADIUS) | |
{ | |
float vis = 1.0f - (dist / VISIBILITY_RADIUS); | |
game->world.visibility[x][z] | |
= fmaxf (game->world.visibility[x][z], vis); | |
game->world.revealed[x][z] = true; | |
} | |
} | |
} | |
} | |
} | |
// Handle world anchoring | |
void | |
HandleWorldAnchoring (GameState *game) | |
{ | |
if (IsKeyPressed (KEY_F) && game->echoEnergy > 0) | |
{ | |
int px = (int)game->playerPosition.x; | |
int pz = (int)game->playerPosition.z; | |
// Anchor area around player | |
for (int x = px - 5; x <= px + 5; x++) | |
{ | |
for (int z = pz - 5; z <= pz + 5; z++) | |
{ | |
if (x >= 0 && x < WORLD_SIZE && z >= 0 && z < WORLD_SIZE) | |
{ | |
float dist = Vector2Distance ((Vector2){ x, z }, | |
(Vector2){ px, pz }); | |
if (dist <= 5 && game->world.revealed[x][z]) | |
{ | |
game->world.anchored[x][z] = true; | |
game->world.visibility[x][z] = 1.0f; | |
} | |
} | |
} | |
} | |
game->echoEnergy--; | |
game->behavior.aggressiveActions++; | |
// Add anchored area | |
if (game->anchoredCount < MAX_ANCHORED_AREAS) | |
{ | |
game->anchoredAreas[game->anchoredCount].position | |
= game->playerPosition; | |
game->anchoredAreas[game->anchoredCount].radius = 5.0f; | |
game->anchoredAreas[game->anchoredCount].active = true; | |
game->anchoredCount++; | |
} | |
// Create effect | |
for (int i = 0; i < 10; i++) | |
{ | |
CreateSoundParticle (game, game->playerPosition, GOLD); | |
} | |
} | |
} | |
// Update enemies | |
void | |
UpdateEnemies (GameState *game) | |
{ | |
// Spawn enemies based on player behavior | |
if (game->behavior.aggressiveActions > 5 && GetRandomValue (0, 100) < 2) | |
{ | |
SpawnEnemy (game, ENEMY_ANGER); | |
} | |
if (game->behavior.noiseLevel < 1.0f && GetRandomValue (0, 100) < 1) | |
{ | |
SpawnEnemy (game, ENEMY_FEAR); | |
} | |
// Update existing enemies | |
for (int i = 0; i < MAX_ENEMIES; i++) | |
{ | |
if (!game->enemies[i].active) | |
continue; | |
Enemy *enemy = &game->enemies[i]; | |
// Only update if enemy is in revealed area | |
if (!IsPositionRevealed (game, enemy->position.x, enemy->position.z)) | |
continue; | |
// Check for time dilation | |
float timeFactor = 1.0f; | |
for (int j = 0; j < 10; j++) | |
{ | |
if (game->timeDilationZones[j].timeLeft > 0) | |
{ | |
float dist = Vector3Distance ( | |
enemy->position, game->timeDilationZones[j].position); | |
if (dist < game->timeDilationZones[j].radius) | |
{ | |
timeFactor = game->timeDilationZones[j].slowFactor; | |
break; | |
} | |
} | |
} | |
// Enemy AI based on type | |
switch (enemy->type) | |
{ | |
case ENEMY_ANGER: | |
// Aggressive - chase player | |
if (Vector3Distance (enemy->position, game->playerPosition) | |
< enemy->detectRadius) | |
{ | |
Vector3 dir = Vector3Normalize ( | |
Vector3Subtract (game->playerPosition, enemy->position)); | |
enemy->velocity = Vector3Scale (dir, enemy->speed * timeFactor); | |
} | |
break; | |
case ENEMY_FEAR: | |
// Elusive - run from player | |
if (Vector3Distance (enemy->position, game->playerPosition) | |
< enemy->detectRadius) | |
{ | |
Vector3 dir = Vector3Normalize ( | |
Vector3Subtract (enemy->position, game->playerPosition)); | |
enemy->velocity = Vector3Scale (dir, enemy->speed * timeFactor); | |
} | |
break; | |
case ENEMY_CURIOSITY: | |
// Curious - circle player | |
if (Vector3Distance (enemy->position, game->playerPosition) | |
< enemy->detectRadius) | |
{ | |
Vector3 toPlayer | |
= Vector3Subtract (game->playerPosition, enemy->position); | |
Vector3 perpendicular = { -toPlayer.z, 0, toPlayer.x }; | |
perpendicular = Vector3Normalize (perpendicular); | |
enemy->velocity | |
= Vector3Scale (perpendicular, enemy->speed * timeFactor); | |
} | |
break; | |
} | |
// Update position | |
Vector3 newPos = Vector3Add ( | |
enemy->position, Vector3Scale (enemy->velocity, game->deltaTime)); | |
// Bounds check | |
if (newPos.x >= 0 && newPos.x < WORLD_SIZE && newPos.z >= 0 | |
&& newPos.z < WORLD_SIZE) | |
{ | |
enemy->position = newPos; | |
enemy->position.y | |
= GetTerrainHeight (game, enemy->position.x, enemy->position.z) | |
+ 1.0f; | |
} | |
// Check collision with player | |
if (Vector3Distance (enemy->position, game->playerPosition) < 1.0f) | |
{ | |
game->echoEnergy = fmaxf (0, game->echoEnergy - 2); | |
} | |
} | |
// Decay behavior values | |
game->behavior.noiseLevel *= 0.99f; | |
} | |
// Spawn enemy | |
void | |
SpawnEnemy (GameState *game, EnemyType type) | |
{ | |
for (int i = 0; i < MAX_ENEMIES; i++) | |
{ | |
if (!game->enemies[i].active) | |
{ | |
Enemy *enemy = &game->enemies[i]; | |
enemy->active = true; | |
enemy->type = type; | |
// Spawn at edge of revealed area | |
float angle = GetRandomValue (0, 360) * DEG2RAD; | |
float dist = ECHO_PULSE_RADIUS + GetRandomValue (5, 10); | |
enemy->position = Vector3Add ( | |
game->playerPosition, | |
(Vector3){ cosf (angle) * dist, 0, sinf (angle) * dist }); | |
// Check bounds before spawning | |
if (enemy->position.x < 0 || enemy->position.x >= WORLD_SIZE | |
|| enemy->position.z < 0 || enemy->position.z >= WORLD_SIZE) | |
{ | |
enemy->active = false; | |
continue; | |
} | |
enemy->position.y | |
= GetTerrainHeight (game, enemy->position.x, enemy->position.z) | |
+ 1.0f; | |
// Set properties based on type | |
switch (type) | |
{ | |
case ENEMY_ANGER: | |
enemy->speed = 4.0f; | |
enemy->detectRadius = 15.0f; | |
break; | |
case ENEMY_FEAR: | |
enemy->speed = 6.0f; | |
enemy->detectRadius = 20.0f; | |
break; | |
case ENEMY_CURIOSITY: | |
enemy->speed = 3.0f; | |
enemy->detectRadius = 10.0f; | |
break; | |
} | |
enemy->velocity = Vector3Zero (); | |
break; | |
} | |
} | |
} | |
// Update crystals | |
void | |
UpdateCrystals (GameState *game) | |
{ | |
for (int i = 0; i < MAX_CRYSTALS; i++) | |
{ | |
if (game->crystals[i].collected) | |
continue; | |
game->crystals[i].glowTimer += game->deltaTime * 2.0f; | |
// Check collection | |
if (Vector3Distance (game->crystals[i].position, game->playerPosition) | |
< 1.5f) | |
{ | |
game->crystals[i].collected = true; | |
game->echoEnergy = fminf (game->echoEnergy + 1, MAX_ECHO_ENERGY); | |
// Create collection effect | |
for (int j = 0; j < 10; j++) | |
{ | |
CreateSoundParticle (game, game->crystals[i].position, SKYBLUE); | |
} | |
} | |
} | |
} | |
// Handle resonance tools | |
void | |
HandleResonanceTools (GameState *game) | |
{ | |
// Tool selection | |
if (IsKeyPressed (KEY_ONE)) | |
game->selectedTool = TOOL_GRAVITY; | |
if (IsKeyPressed (KEY_TWO)) | |
game->selectedTool = TOOL_TIME_DILATION; | |
if (IsKeyPressed (KEY_THREE)) | |
game->selectedTool = TOOL_ECHO_BRIDGE; | |
// Tool usage | |
if (IsMouseButtonPressed (MOUSE_LEFT_BUTTON) && game->echoEnergy > 0) | |
{ | |
Ray ray = GetMouseRay (GetMousePosition (), | |
game->cameraMode == VIEW_FIRST_PERSON | |
? game->camera | |
: game->isometricCamera); | |
// Simple ground intersection | |
if (ray.direction.y < 0) | |
{ | |
float t = -ray.position.y / ray.direction.y; | |
Vector3 hitPoint | |
= Vector3Add (ray.position, Vector3Scale (ray.direction, t)); | |
switch (game->selectedTool) | |
{ | |
case TOOL_GRAVITY: | |
ApplyGravityManipulation ( | |
game, hitPoint, IsKeyDown (KEY_LEFT_SHIFT) ? -1.0f : 1.0f); | |
game->echoEnergy--; | |
game->behavior.aggressiveActions++; | |
break; | |
case TOOL_TIME_DILATION: | |
CreateTimeDilationZone (game, hitPoint); | |
game->echoEnergy--; | |
break; | |
case TOOL_ECHO_BRIDGE: | |
{ | |
static Vector3 bridgeStart = { 0 }; | |
static bool placingBridge = false; | |
if (!placingBridge) | |
{ | |
bridgeStart = hitPoint; | |
placingBridge = true; | |
} | |
else | |
{ | |
CreateEchoBridge (game, bridgeStart, hitPoint); | |
placingBridge = false; | |
game->echoEnergy--; | |
} | |
break; | |
} | |
default: | |
break; | |
} | |
} | |
} | |
} | |
// Apply gravity manipulation | |
void | |
ApplyGravityManipulation (GameState *game, Vector3 position, float strength) | |
{ | |
int cx = (int)position.x; | |
int cz = (int)position.z; | |
float radius = 5.0f; | |
for (int x = cx - radius; x <= cx + radius; x++) | |
{ | |
for (int z = cz - radius; z <= cz + radius; z++) | |
{ | |
if (x >= 0 && x < WORLD_SIZE && z >= 0 && z < WORLD_SIZE) | |
{ | |
float dist | |
= Vector2Distance ((Vector2){ x, z }, (Vector2){ cx, cz }); | |
if (dist <= radius) | |
{ | |
float factor = 1.0f - (dist / radius); | |
game->world.heights[x][z] += strength * factor * 2.0f; | |
game->world.memory[x][z] += fabsf (strength) * 0.5f; | |
} | |
} | |
} | |
} | |
// Create effect | |
for (int i = 0; i < 20; i++) | |
{ | |
CreateSoundParticle (game, position, strength > 0 ? GREEN : RED); | |
} | |
} | |
// Create time dilation zone | |
void | |
CreateTimeDilationZone (GameState *game, Vector3 position) | |
{ | |
for (int i = 0; i < 10; i++) | |
{ | |
if (game->timeDilationZones[i].timeLeft <= 0) | |
{ | |
game->timeDilationZones[i].position = position; | |
game->timeDilationZones[i].radius = 8.0f; | |
game->timeDilationZones[i].slowFactor = 0.3f; | |
game->timeDilationZones[i].timeLeft = 10.0f; | |
// Create effect | |
for (int j = 0; j < 15; j++) | |
{ | |
CreateSoundParticle (game, position, PURPLE); | |
} | |
break; | |
} | |
} | |
} | |
// Create echo bridge | |
void | |
CreateEchoBridge (GameState *game, Vector3 start, Vector3 end) | |
{ | |
if (game->bridgeCount < MAX_ECHO_BRIDGES) | |
{ | |
game->echoBridges[game->bridgeCount].start = start; | |
game->echoBridges[game->bridgeCount].end = end; | |
game->echoBridges[game->bridgeCount].active = true; | |
game->echoBridges[game->bridgeCount].width = 2.0f; | |
game->bridgeCount++; | |
// Reveal and anchor bridge path | |
Vector3 dir = Vector3Normalize (Vector3Subtract (end, start)); | |
float length = Vector3Distance (start, end); | |
for (float t = 0; t < length; t += 0.5f) | |
{ | |
Vector3 point = Vector3Add (start, Vector3Scale (dir, t)); | |
int x = (int)point.x; | |
int z = (int)point.z; | |
if (x >= 0 && x < WORLD_SIZE && z >= 0 && z < WORLD_SIZE) | |
{ | |
game->world.revealed[x][z] = true; | |
game->world.anchored[x][z] = true; | |
game->world.visibility[x][z] = 1.0f; | |
} | |
} | |
// Create effect | |
for (int i = 0; i < 30; i++) | |
{ | |
float t = (float)i / 30.0f; | |
Vector3 point = Vector3Lerp (start, end, t); | |
CreateSoundParticle (game, point, YELLOW); | |
} | |
} | |
} | |
// Update time dilation zones | |
void | |
UpdateTimeDilationZones (GameState *game) | |
{ | |
for (int i = 0; i < 10; i++) | |
{ | |
if (game->timeDilationZones[i].timeLeft > 0) | |
{ | |
game->timeDilationZones[i].timeLeft -= game->deltaTime; | |
} | |
} | |
} | |
// Update sound particles | |
void | |
UpdateSoundParticles (GameState *game) | |
{ | |
for (int i = 0; i < MAX_SOUND_PARTICLES; i++) | |
{ | |
if (game->soundParticles[i].life > 0) | |
{ | |
game->soundParticles[i].life -= game->deltaTime; | |
game->soundParticles[i].position = Vector3Add ( | |
game->soundParticles[i].position, | |
Vector3Scale (game->soundParticles[i].velocity, game->deltaTime)); | |
game->soundParticles[i].velocity.y -= 2.0f * game->deltaTime; | |
} | |
} | |
} | |
// Create sound particle | |
void | |
CreateSoundParticle (GameState *game, Vector3 position, Color color) | |
{ | |
for (int i = 0; i < MAX_SOUND_PARTICLES; i++) | |
{ | |
if (game->soundParticles[i].life <= 0) | |
{ | |
game->soundParticles[i].position = position; | |
game->soundParticles[i].velocity | |
= (Vector3){ (float)(GetRandomValue (-100, 100)) / 50.0f, | |
(float)(GetRandomValue (100, 300)) / 100.0f, | |
(float)(GetRandomValue (-100, 100)) / 50.0f }; | |
game->soundParticles[i].life = 1.0f; | |
game->soundParticles[i].color = color; | |
break; | |
} | |
} | |
} | |
// Update world memory | |
void | |
UpdateWorldMemory (GameState *game) | |
{ | |
// Check for instability from overuse | |
for (int x = 0; x < WORLD_SIZE; x++) | |
{ | |
for (int z = 0; z < WORLD_SIZE; z++) | |
{ | |
if (game->world.memory[x][z] > 5.0f) | |
{ | |
// Terrain instability | |
if (GetRandomValue (0, 100) < 1) | |
{ | |
game->world.heights[x][z] | |
+= (float)(GetRandomValue (-10, 10)) / 10.0f; | |
// Spawn anger enemy | |
if (GetRandomValue (0, 100) < 10) | |
{ | |
SpawnEnemy (game, ENEMY_ANGER); | |
} | |
} | |
} | |
// Decay memory slowly | |
game->world.memory[x][z] *= 0.999f; | |
} | |
} | |
} | |
// Get terrain height at position | |
float | |
GetTerrainHeight (GameState *game, float x, float z) | |
{ | |
int ix = (int)x; | |
int iz = (int)z; | |
if (ix < 0 || ix >= WORLD_SIZE - 1 || iz < 0 || iz >= WORLD_SIZE - 1) | |
{ | |
return 0; | |
} | |
// Bilinear interpolation | |
float fx = x - ix; | |
float fz = z - iz; | |
float h00 = game->world.heights[ix][iz]; | |
float h10 = game->world.heights[ix + 1][iz]; | |
float h01 = game->world.heights[ix][iz + 1]; | |
float h11 = game->world.heights[ix + 1][iz + 1]; | |
float h0 = h00 * (1 - fx) + h10 * fx; | |
float h1 = h01 * (1 - fx) + h11 * fx; | |
return h0 * (1 - fz) + h1 * fz; | |
} | |
// Check if position is revealed | |
bool | |
IsPositionRevealed (GameState *game, float x, float z) | |
{ | |
int ix = (int)x; | |
int iz = (int)z; | |
if (ix < 0 || ix >= WORLD_SIZE || iz < 0 || iz >= WORLD_SIZE) | |
{ | |
return false; | |
} | |
return game->world.revealed[ix][iz]; | |
} | |
// Check if position is anchored | |
bool | |
IsPositionAnchored (GameState *game, float x, float z) | |
{ | |
int ix = (int)x; | |
int iz = (int)z; | |
if (ix < 0 || ix >= WORLD_SIZE || iz < 0 || iz >= WORLD_SIZE) | |
{ | |
return false; | |
} | |
return game->world.anchored[ix][iz]; | |
} | |
// Draw game | |
void | |
DrawGame (GameState *game) | |
{ | |
BeginDrawing (); | |
ClearBackground (BLACK); | |
// Draw 3D scene | |
BeginMode3D (game->cameraMode == VIEW_FIRST_PERSON ? game->camera | |
: game->isometricCamera); | |
DrawWorld (game); | |
DrawCrystals (game); | |
DrawEnemies (game); | |
DrawTimeDilationZones (game); | |
DrawEchoBridges (game); | |
DrawSoundParticles (game); | |
if (game->pulsing) | |
{ | |
DrawEchoPulseEffect (game); | |
} | |
EndMode3D (); | |
// Draw UI on top | |
DrawUI (game); | |
// Draw starting hint if player hasn't used echo pulse yet | |
static int firstPulseUsed = 0; | |
if (!firstPulseUsed && game->echoEnergy == MAX_ECHO_ENERGY) | |
{ | |
DrawText ("Press SPACE to reveal the world with Echo Pulse!", | |
SCREEN_WIDTH / 2 - 250, SCREEN_HEIGHT / 2, 25, WHITE); | |
if (game->pulsing) | |
firstPulseUsed = 1; | |
} | |
EndDrawing (); | |
} | |
// Draw world | |
void | |
DrawWorld (GameState *game) | |
{ | |
for (int x = 0; x < WORLD_SIZE - 1; x++) | |
{ | |
for (int z = 0; z < WORLD_SIZE - 1; z++) | |
{ | |
if (game->world.visibility[x][z] <= 0) | |
continue; | |
float vis = game->world.visibility[x][z]; | |
Color color; | |
if (game->world.anchored[x][z]) | |
{ | |
color = ColorAlpha (GOLD, vis); | |
} | |
else if (game->pulsing && game->cameraMode == VIEW_ISOMETRIC) | |
{ | |
// Wireframe during pulse | |
color = ColorAlpha (SKYBLUE, vis * 0.5f); | |
} | |
else | |
{ | |
// Normal terrain color | |
float height = game->world.heights[x][z]; | |
int g = 50 + (int)(height * 10); | |
if (g < 0) | |
g = 0; | |
if (g > 255) | |
g = 255; | |
color = ColorAlpha ((Color){ g / 2, g, g / 3, 255 }, vis); | |
} | |
// Draw terrain quad | |
Vector3 v1 = { x, game->world.heights[x][z], z }; | |
Vector3 v2 = { x + 1, game->world.heights[x + 1][z], z }; | |
Vector3 v3 = { x + 1, game->world.heights[x + 1][z + 1], z + 1 }; | |
Vector3 v4 = { x, game->world.heights[x][z + 1], z + 1 }; | |
if (game->pulsing && game->cameraMode == VIEW_ISOMETRIC) | |
{ | |
// Draw wireframe | |
DrawLine3D (v1, v2, color); | |
DrawLine3D (v2, v3, color); | |
DrawLine3D (v3, v4, color); | |
DrawLine3D (v4, v1, color); | |
} | |
else | |
{ | |
// Draw solid | |
DrawTriangle3D (v1, v2, v3, color); | |
DrawTriangle3D (v1, v3, v4, color); | |
} | |
} | |
} | |
} | |
// Draw echo pulse effect | |
void | |
DrawEchoPulseEffect (GameState *game) | |
{ | |
int segments = 32; | |
float angleStep = 2 * PI / segments; | |
for (int i = 0; i < segments; i++) | |
{ | |
float angle1 = i * angleStep; | |
float angle2 = (i + 1) * angleStep; | |
Vector3 p1 = { game->currentPulse.position.x | |
+ cosf (angle1) * game->currentPulse.radius, | |
game->currentPulse.position.y, | |
game->currentPulse.position.z | |
+ sinf (angle1) * game->currentPulse.radius }; | |
Vector3 p2 = { game->currentPulse.position.x | |
+ cosf (angle2) * game->currentPulse.radius, | |
game->currentPulse.position.y, | |
game->currentPulse.position.z | |
+ sinf (angle2) * game->currentPulse.radius }; | |
p1.y = GetTerrainHeight (game, p1.x, p1.z) + 0.5f; | |
p2.y = GetTerrainHeight (game, p2.x, p2.z) + 0.5f; | |
DrawLine3D (p1, p2, ColorAlpha (SKYBLUE, game->currentPulse.strength)); | |
// Vertical lines | |
Vector3 p1Top = p1; | |
p1Top.y += 5.0f * game->currentPulse.strength; | |
DrawLine3D (p1, p1Top, | |
ColorAlpha (SKYBLUE, game->currentPulse.strength * 0.5f)); | |
} | |
} | |
// Draw crystals | |
void | |
DrawCrystals (GameState *game) | |
{ | |
for (int i = 0; i < MAX_CRYSTALS; i++) | |
{ | |
if (game->crystals[i].collected) | |
continue; | |
Vector3 pos = game->crystals[i].position; | |
// Only draw if visible | |
if (!IsPositionRevealed (game, pos.x, pos.z)) | |
continue; | |
float vis = game->world.visibility[(int)pos.x][(int)pos.z]; | |
if (vis <= 0) | |
continue; | |
// Glowing effect | |
float glow = (sinf (game->crystals[i].glowTimer) + 1.0f) * 0.5f; | |
Color color = ColorAlpha (SKYBLUE, vis * (0.5f + glow * 0.5f)); | |
DrawCube (pos, 0.3f, 0.6f, 0.3f, color); | |
DrawCubeWires (pos, 0.35f, 0.65f, 0.35f, ColorAlpha (WHITE, vis * glow)); | |
} | |
} | |
// Draw enemies | |
void | |
DrawEnemies (GameState *game) | |
{ | |
for (int i = 0; i < MAX_ENEMIES; i++) | |
{ | |
if (!game->enemies[i].active) | |
continue; | |
Enemy *enemy = &game->enemies[i]; | |
// Only draw if visible | |
if (!IsPositionRevealed (game, enemy->position.x, enemy->position.z)) | |
continue; | |
float vis | |
= game->world | |
.visibility[(int)enemy->position.x][(int)enemy->position.z]; | |
if (vis <= 0) | |
continue; | |
Color color; | |
switch (enemy->type) | |
{ | |
case ENEMY_ANGER: | |
color = ColorAlpha (RED, vis); | |
DrawSphere (enemy->position, 0.5f, color); | |
break; | |
case ENEMY_FEAR: | |
color = ColorAlpha (DARKBLUE, vis * 0.5f); | |
DrawCube (enemy->position, 0.4f, 0.4f, 0.4f, color); | |
break; | |
case ENEMY_CURIOSITY: | |
color = ColorAlpha (YELLOW, vis); | |
DrawCylinder (enemy->position, 0.3f, 0.3f, 0.6f, 8, color); | |
break; | |
} | |
} | |
} | |
// Draw time dilation zones | |
void | |
DrawTimeDilationZones (GameState *game) | |
{ | |
for (int i = 0; i < 10; i++) | |
{ | |
if (game->timeDilationZones[i].timeLeft <= 0) | |
continue; | |
TimeDilationZone *zone = &game->timeDilationZones[i]; | |
float alpha = zone->timeLeft / 10.0f; | |
DrawCylinderWires (zone->position, zone->radius, zone->radius, 0.1f, 16, | |
ColorAlpha (PURPLE, alpha * 0.3f)); | |
// Draw rotating rings | |
float angle = GetTime () * 0.5f; | |
int rings = 3; | |
for (int r = 0; r < rings; r++) | |
{ | |
float ringRadius = zone->radius * (r + 1) / rings; | |
int segments = 16; | |
for (int s = 0; s < segments; s++) | |
{ | |
float a1 = (s * 2 * PI / segments) + angle * (r + 1); | |
float a2 = ((s + 1) * 2 * PI / segments) + angle * (r + 1); | |
Vector3 p1 = Vector3Add ( | |
zone->position, (Vector3){ cosf (a1) * ringRadius, r * 0.5f, | |
sinf (a1) * ringRadius }); | |
Vector3 p2 = Vector3Add ( | |
zone->position, (Vector3){ cosf (a2) * ringRadius, r * 0.5f, | |
sinf (a2) * ringRadius }); | |
DrawLine3D (p1, p2, ColorAlpha (PURPLE, alpha * 0.5f)); | |
} | |
} | |
} | |
} | |
// Draw echo bridges | |
void | |
DrawEchoBridges (GameState *game) | |
{ | |
for (int i = 0; i < game->bridgeCount; i++) | |
{ | |
if (!game->echoBridges[i].active) | |
continue; | |
EchoBridge *bridge = &game->echoBridges[i]; | |
// Draw bridge segments | |
Vector3 dir | |
= Vector3Normalize (Vector3Subtract (bridge->end, bridge->start)); | |
Vector3 perpendicular = Vector3Normalize ((Vector3){ -dir.z, 0, dir.x }); | |
float length = Vector3Distance (bridge->start, bridge->end); | |
int segments = (int)(length / 0.5f); | |
for (int s = 0; s < segments; s++) | |
{ | |
float t1 = (float)s / segments; | |
float t2 = (float)(s + 1) / segments; | |
Vector3 p1 = Vector3Lerp (bridge->start, bridge->end, t1); | |
Vector3 p2 = Vector3Lerp (bridge->start, bridge->end, t2); | |
p1.y = GetTerrainHeight (game, p1.x, p1.z) + 0.1f; | |
p2.y = GetTerrainHeight (game, p2.x, p2.z) + 0.1f; | |
// Draw bridge width | |
for (float w = -bridge->width / 2; w <= bridge->width / 2; w += 0.5f) | |
{ | |
Vector3 offset = Vector3Scale (perpendicular, w); | |
Vector3 p1w = Vector3Add (p1, offset); | |
Vector3 p2w = Vector3Add (p2, offset); | |
DrawLine3D (p1w, p2w, ColorAlpha (YELLOW, 0.8f)); | |
} | |
// Draw cross beams | |
if (s % 2 == 0) | |
{ | |
Vector3 p1Left = Vector3Add ( | |
p1, Vector3Scale (perpendicular, -bridge->width / 2)); | |
Vector3 p1Right = Vector3Add ( | |
p1, Vector3Scale (perpendicular, bridge->width / 2)); | |
DrawLine3D (p1Left, p1Right, ColorAlpha (YELLOW, 0.6f)); | |
} | |
} | |
} | |
} | |
// Draw sound particles | |
void | |
DrawSoundParticles (GameState *game) | |
{ | |
for (int i = 0; i < MAX_SOUND_PARTICLES; i++) | |
{ | |
if (game->soundParticles[i].life > 0) | |
{ | |
float alpha = game->soundParticles[i].life; | |
Color color = ColorAlpha (game->soundParticles[i].color, alpha); | |
DrawSphere (game->soundParticles[i].position, 0.1f, color); | |
} | |
} | |
} | |
// Draw UI | |
void | |
DrawUI (GameState *game) | |
{ | |
// Echo energy bar | |
DrawRectangle (10, 10, 200, 30, ColorAlpha (BLACK, 0.5f)); | |
DrawRectangle (10, 10, 200 * game->echoEnergy / MAX_ECHO_ENERGY, 30, SKYBLUE); | |
DrawRectangleLines (10, 10, 200, 30, WHITE); | |
DrawText ( | |
TextFormat ("Echo Energy: %d/%d", game->echoEnergy, MAX_ECHO_ENERGY), 15, | |
15, 20, WHITE); | |
// Selected tool | |
const char *toolName = "None"; | |
switch (game->selectedTool) | |
{ | |
case TOOL_GRAVITY: | |
toolName = "Gravity Manipulation"; | |
break; | |
case TOOL_TIME_DILATION: | |
toolName = "Time Dilation"; | |
break; | |
case TOOL_ECHO_BRIDGE: | |
toolName = "Echo Bridge"; | |
break; | |
default: | |
break; | |
} | |
DrawText (TextFormat ("Tool: %s", toolName), 10, 50, 20, WHITE); | |
// Controls | |
DrawText ("Controls:", 10, SCREEN_HEIGHT - 170, 20, WHITE); | |
DrawText ("WASD/QE - Move | A/D - Rotate", 10, SCREEN_HEIGHT - 140, 18, GRAY); | |
DrawText ("SPACE - Echo Pulse | F - Anchor Area", 10, SCREEN_HEIGHT - 120, 18, | |
GRAY); | |
DrawText ("1/2/3 - Select Tool | LMB - Use Tool", 10, SCREEN_HEIGHT - 100, 18, | |
GRAY); | |
DrawText ("SHIFT + LMB - Reverse Gravity Tool", 10, SCREEN_HEIGHT - 80, 18, | |
GRAY); | |
DrawText ("F11 - Toggle Fullscreen | ESC - Exit", 10, SCREEN_HEIGHT - 60, 18, | |
GRAY); | |
// World memory warning | |
float maxMemory = 0; | |
for (int x = 0; x < WORLD_SIZE; x++) | |
{ | |
for (int z = 0; z < WORLD_SIZE; z++) | |
{ | |
if (game->world.memory[x][z] > maxMemory) | |
{ | |
maxMemory = game->world.memory[x][z]; | |
} | |
} | |
} | |
if (maxMemory > 3.0f) | |
{ | |
DrawText ("WARNING: World Memory Unstable!", SCREEN_WIDTH / 2 - 150, 50, | |
20, ColorAlpha (RED, sinf (GetTime () * 5) * 0.5f + 0.5f)); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment