From a2ffb45fc552a93de9e83ea27d0620a095ee71d7 Mon Sep 17 00:00:00 2001 From: huaqo Date: Sat, 19 Sep 2026 13:17:46 +0200 Subject: [PATCH] feat: add wake, rose, terrain, pathfinding, gameover, clouds, enemy shooting, variable bird speed refactor: some things --- shaders/clouds.fs | 69 ++++++++ shaders/clouds.vs | 14 ++ shaders/sunrays.fs | 45 +++++ shaders/terrain.fs | 53 ++++++ shaders/terrain.vs | 16 ++ shaders/wake.fs | 79 +++++++++ shaders/wake.vs | 14 ++ shaders/water.fs | 32 +--- shaders/water.vs | 3 +- src/app.c | 38 +++-- src/appGame.c | 156 +++++++++++------ src/appGameBirds.c | 157 +++++++++-------- src/appGameClouds.c | 63 +++++++ src/appGameCollisions.c | 178 +++++++++++++------ src/appGameEnemies.c | 302 +++++++++++++++++++++++++++----- src/appGameFloor.c | 206 +++++++++++++--------- src/appGameIslands.c | 218 +++++++++++++++++++---- src/appGameOver.c | 60 +++++++ src/appGameOverlay.c | 35 ++-- src/appGamePathfinding.c | 273 +++++++++++++++++++++++++++++ src/appGamePlayer.c | 180 +++++++++++-------- src/appGameProjectiles.c | 115 +++++++------ src/appGameRose.c | 59 +++++++ src/appGameTerrain.c | 286 +++++++++++++++++++++++++++++++ src/appGameWake.c | 120 +++++++++++++ src/appInput.c | 28 +++ src/appLogo.c | 5 +- src/appMenu.c | 69 ++++---- src/appMenuMain.c | 332 ++++++++++++++++-------------------- src/appMenuMainBackground.c | 85 ++++----- src/appMenuSettings.c | 92 +++++----- src/appState.c | 14 +- src/engineAnimation.c | 22 +-- src/engineArray.c | 3 +- src/engineAssets.c | 78 +++++++++ src/engineCamera.c | 28 ++- src/engineConfig.c | 43 ++--- src/engineFont.c | 13 +- src/engineGamegui.c | 72 -------- src/engineGui.c | 107 ++++++++++-- src/engineInput.c | 7 +- src/engineObject.c | 8 +- src/enginePhysics.c | 64 +++---- src/enginePostfx.c | 67 +++++--- src/engineRectangle.c | 58 +++---- src/engineSoundtrack.c | 2 +- src/engineSprite.c | 65 +++---- src/engineStorage.c | 103 +++++------ src/engineVector.c | 44 ++++- 49 files changed, 3002 insertions(+), 1178 deletions(-) create mode 100644 shaders/clouds.fs create mode 100644 shaders/clouds.vs create mode 100644 shaders/sunrays.fs create mode 100644 shaders/terrain.fs create mode 100644 shaders/terrain.vs create mode 100644 shaders/wake.fs create mode 100644 shaders/wake.vs create mode 100644 src/appGameClouds.c create mode 100644 src/appGameOver.c create mode 100644 src/appGamePathfinding.c create mode 100644 src/appGameRose.c create mode 100644 src/appGameTerrain.c create mode 100644 src/appGameWake.c create mode 100644 src/appInput.c create mode 100644 src/engineAssets.c delete mode 100644 src/engineGamegui.c diff --git a/shaders/clouds.fs b/shaders/clouds.fs new file mode 100644 index 0000000..7333590 --- /dev/null +++ b/shaders/clouds.fs @@ -0,0 +1,69 @@ +#version 330 + +in vec2 fragWorldPos; +out vec4 finalColor; + +uniform float time; +uniform float cloudScale = 0.018; +uniform vec2 driftDirection = vec2(1.0, 0.35); +uniform float driftSpeed = 4.0; +uniform float coverage = 0.99; +uniform float softness = 0.12; +uniform vec4 cloudColor = vec4(1.0, 1.0, 1.0, 0.8); + +uniform bool isShadowPass = false; +uniform vec2 shadowWorldOffset = vec2(48.0, 72.0); +uniform vec4 shadowColor = vec4(0.0, 0.05, 0.1, 0.35); + +vec2 pixelizeCoordinates(vec2 coordinates) +{ + return floor(coordinates); +} + +float hash(vec2 p) +{ + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float valueNoise(vec2 p) +{ + vec2 i = floor(p); + vec2 f = fract(p); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +float cloudField(vec2 p) +{ + float value = 0.0; + float amplitude = 0.5; + for (int i = 0; i < 4; i++) { + value += amplitude * valueNoise(p); + p *= 2.0; + amplitude *= 0.5; + } + return value; +} + +void main() +{ + vec2 worldPos = fragWorldPos; + if (isShadowPass) { + worldPos -= shadowWorldOffset; + } + + vec2 pixelized = pixelizeCoordinates(worldPos); + vec2 drift = normalize(driftDirection) * time * driftSpeed; + float density = cloudField((pixelized + drift) * cloudScale); + + float mask = smoothstep(coverage - softness, coverage + softness, density); + + vec4 layerColor = isShadowPass ? shadowColor : cloudColor; + finalColor = vec4(layerColor.rgb, layerColor.a * mask); +} diff --git a/shaders/clouds.vs b/shaders/clouds.vs new file mode 100644 index 0000000..fa4de7d --- /dev/null +++ b/shaders/clouds.vs @@ -0,0 +1,14 @@ +#version 330 +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec4 vertexColor; + +out vec2 fragWorldPos; + +uniform mat4 mvp; + +void main() +{ + fragWorldPos = vertexPosition.xy; + gl_Position = mvp * vec4(vertexPosition, 1.0); +} diff --git a/shaders/sunrays.fs b/shaders/sunrays.fs new file mode 100644 index 0000000..ddfd1fe --- /dev/null +++ b/shaders/sunrays.fs @@ -0,0 +1,45 @@ +#version 330 + +uniform sampler2D texture0; +uniform vec2 resolution; +uniform float time; + +// Screen-space UV of the sun — kept off in a corner like a low sun rather +// than dead center, so the rays read as directional light rather than a +// starburst sitting on top of the view. This shader samples the already- +// rendered scene texture (RenderPostFX draws it via a source rect with a +// negative height to correct raylib's render-texture V-flip for texture0 +// itself), but fragTexCoord's V still runs top-of-screen=0 in that same +// flipped sense — so a Y near 0 here actually lands near the BOTTOM of +// what's on screen; 0.88 is what puts the sun near the top. +uniform vec2 sunScreenPos = vec2(0.82, 0.88); +uniform float rayCount = 10.0; +uniform float raySharpness = 3.0; +uniform float rayStrength = 0.35; +uniform float raySpeed = 0.03; + +in vec2 fragTexCoord; +out vec4 finalColor; + +void main() +{ + vec2 uv = fragTexCoord; + vec4 baseColor = texture(texture0, uv); + + // Cheap screen-space light shafts: no scene depth/occlusion to sample, + // so this fakes them as angular streaks radiating from the sun position + // that fade out with distance, rather than true raymarched shafts. + vec2 aspectFix = vec2(resolution.x / resolution.y, 1.0); + vec2 toSun = (uv - sunScreenPos) * aspectFix; + float dist = length(toSun); + float angle = atan(toSun.y, toSun.x); + + float streaks = sin(angle * rayCount + time * raySpeed) * 0.5 + 0.5; + streaks = pow(streaks, raySharpness); + + float falloff = clamp(1.0 - dist * 0.9, 0.0, 1.0); + falloff *= falloff; + + vec3 rayColor = vec3(1.0, 0.92, 0.75) * streaks * falloff * rayStrength; + finalColor = vec4(baseColor.rgb + rayColor, baseColor.a); +} diff --git a/shaders/terrain.fs b/shaders/terrain.fs new file mode 100644 index 0000000..9296d49 --- /dev/null +++ b/shaders/terrain.fs @@ -0,0 +1,53 @@ +#version 330 + +in vec2 fragTexCoord; +in vec2 fragWorldPos; +out vec4 finalColor; + +uniform sampler2D texture0; +uniform float time; + +uniform vec4 foamColor = vec4(0.96, 0.99, 1.0, 1.0); +uniform float foamAnimSpeed = 1.6; +uniform float foamNoiseScale = 0.55; + +float hash(vec2 p) +{ + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float valueNoise(vec2 p) +{ + vec2 i = floor(p); + vec2 f = fract(p); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +void main() +{ + vec4 tex = texture(texture0, fragTexCoord); + + if (tex.a >= 0.999) { + finalColor = tex; + return; + } + + if (tex.a <= 0.001) { + discard; + } + + vec2 pixelized = floor(fragWorldPos); + float n1 = valueNoise(pixelized * foamNoiseScale + vec2(time * foamAnimSpeed, 0.0)); + float n2 = valueNoise(pixelized * foamNoiseScale * 2.1 - vec2(0.0, time * foamAnimSpeed * 0.7)); + float churn = n1 * 0.6 + n2 * 0.4; + + float mask = tex.a * smoothstep(0.2, 0.7, churn + tex.a * 0.6); + finalColor = vec4(foamColor.rgb, foamColor.a * mask); +} diff --git a/shaders/terrain.vs b/shaders/terrain.vs new file mode 100644 index 0000000..365bec4 --- /dev/null +++ b/shaders/terrain.vs @@ -0,0 +1,16 @@ +#version 330 +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec4 vertexColor; + +out vec2 fragTexCoord; +out vec2 fragWorldPos; + +uniform mat4 mvp; + +void main() +{ + fragTexCoord = vertexTexCoord; + fragWorldPos = vertexPosition.xy; + gl_Position = mvp * vec4(vertexPosition, 1.0); +} diff --git a/shaders/wake.fs b/shaders/wake.fs new file mode 100644 index 0000000..0386029 --- /dev/null +++ b/shaders/wake.fs @@ -0,0 +1,79 @@ +#version 330 + +in vec2 fragWorldPos; +out vec4 finalColor; + +uniform float time; + +#define TRAIL_POINTS 12 +uniform vec2 trailPoints[TRAIL_POINTS]; +uniform float trailAges[TRAIL_POINTS]; +uniform int trailCount; +uniform float wakeIntensity; + +uniform vec4 foamColor = vec4(0.96, 0.99, 1.0, 1.0); +uniform float wakeHalfWidthStart = 2.0; +uniform float wakeHalfWidthEnd = 7.0; +uniform float foamNoiseScale = 0.5; +uniform float foamAnimSpeed = 2.4; + +float hash(vec2 p) +{ + p = fract(p * vec2(123.34, 456.21)); + p += dot(p, p + 45.32); + return fract(p.x * p.y); +} + +float valueNoise(vec2 p) +{ + vec2 i = floor(p); + vec2 f = fract(p); + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + vec2 u = f * f * (3.0 - 2.0 * f); + return mix(a, b, u.x) + (c - a) * u.y * (1.0 - u.x) + (d - b) * u.x * u.y; +} + +float segmentDistance(vec2 p, vec2 a, vec2 b, out float segT) +{ + vec2 ab = b - a; + float lenSq = dot(ab, ab); + segT = lenSq > 0.0001 ? clamp(dot(p - a, ab) / lenSq, 0.0, 1.0) : 0.0; + vec2 closest = a + ab * segT; + return length(p - closest); +} + +void main() +{ + if (trailCount < 2) discard; + + float bestDist = 1e9; + float bestAge = 0.0; + for (int i = 0; i < TRAIL_POINTS - 1; i++) { + if (i >= trailCount - 1) break; + float segT; + float d = segmentDistance(fragWorldPos, trailPoints[i], trailPoints[i + 1], segT); + if (d < bestDist) { + bestDist = d; + bestAge = mix(trailAges[i], trailAges[i + 1], segT); + } + } + + float halfWidth = mix(wakeHalfWidthStart, wakeHalfWidthEnd, bestAge); + if (bestDist > halfWidth) discard; + + float edgeFade = 1.0 - smoothstep(halfWidth * 0.55, halfWidth, bestDist); + float ageFade = 1.0 - bestAge; + float envelope = edgeFade * ageFade; + + vec2 pixelized = floor(fragWorldPos); + float n = valueNoise(pixelized * foamNoiseScale + vec2(time * foamAnimSpeed, 0.0)); + float churn = mix(0.55, 1.0, n); + + float speedScale = mix(0.65, 1.0, wakeIntensity); + + float mask = envelope * churn * speedScale; + finalColor = vec4(foamColor.rgb, foamColor.a * mask); +} diff --git a/shaders/wake.vs b/shaders/wake.vs new file mode 100644 index 0000000..fa4de7d --- /dev/null +++ b/shaders/wake.vs @@ -0,0 +1,14 @@ +#version 330 +in vec3 vertexPosition; +in vec2 vertexTexCoord; +in vec4 vertexColor; + +out vec2 fragWorldPos; + +uniform mat4 mvp; + +void main() +{ + fragWorldPos = vertexPosition.xy; + gl_Position = mvp * vec4(vertexPosition, 1.0); +} diff --git a/shaders/water.fs b/shaders/water.fs index 6cb7fce..2dc7f60 100644 --- a/shaders/water.fs +++ b/shaders/water.fs @@ -1,17 +1,12 @@ #version 330 -// ----------------------------- -// Inputs in vec2 fragTexCoord; in vec4 fragColor; +in vec2 fragWorldPos; -// ----------------------------- -// Outputs out vec4 finalColor; -// ----------------------------- -// Uniforms -uniform sampler2D texture0; // main texture +uniform sampler2D texture0; uniform sampler2D waterDepthGradient; uniform sampler2D causticTexture; uniform sampler2D causticHighlightTexture; @@ -24,6 +19,7 @@ uniform sampler2D foamTexture; uniform float aspectRatio = 1.0; uniform float pixelization = 2048.0; +uniform float noiseCoordinateScale = 2048.0; uniform vec4 causticColor = vec4(0.455, 0.773, 0.765, 1.0); uniform vec4 causticHighlightColor = vec4(0.741, 0.894, 0.898, 1.0); @@ -46,9 +42,6 @@ uniform float generalTransparency = 1.0; uniform float time; -// ----------------------------- -// Helper functions - vec2 blendSubtract_vec2(vec2 base, vec2 blend, float opacity) { vec2 result = base - blend; @@ -80,9 +73,6 @@ vec2 applyAspectRatio(vec2 coordinates) return vec2(coordinates.x, coordinates.y * aspectRatio); } -// ----------------------------- -// Shader layers - vec4 caustics(vec2 pixelizedCoordinates) { vec4 causticNoise = texture(causticNoiseTexture, time * causticSpeed + pixelizedCoordinates); @@ -111,27 +101,23 @@ vec4 foam(vec2 pixelizedCoordinates, vec4 mainTexColor) return vec4(colorizedFoam.rgb, colorizedFoam.a * intensity); } -// ----------------------------- -// Main fragment - void main() { - vec2 pixelizedCoordinates = pixelizeCoordinates(applyAspectRatio(fragTexCoord)); + vec2 pixelizedCoordinates = pixelizeCoordinates(fragWorldPos / noiseCoordinateScale); vec4 mainTex = texture(texture0, fragTexCoord); vec4 depthBasedWaterColor = texture(waterDepthGradient, vec2(1.0 - mainTex.b, 1.0)); - - vec4 finalCaustics = caustics(pixelizedCoordinates); + + vec4 finalCaustics = caustics(pixelizedCoordinates); vec4 finalSpecular = specular(pixelizedCoordinates); vec4 finalFoam = foam(pixelizedCoordinates, mainTex); - + vec4 waterWithCausticLayer = mix(depthBasedWaterColor, finalCaustics, finalCaustics.a); vec4 waterWithCausticAndSpecularLayer = mix(waterWithCausticLayer, finalSpecular, ceil(finalCaustics.a) * finalSpecular.a); vec4 waterWithCausticAndSpecularAndFoamLayer = mix(waterWithCausticAndSpecularLayer, finalFoam, finalFoam.a); - + float outline = mainTex.a * mainTex.r; vec4 finalOutlineColor = outline * outlineColor; - + vec4 finalRGBColor = mix(waterWithCausticAndSpecularAndFoamLayer, finalOutlineColor, outline); finalColor = vec4(finalRGBColor.rgb, mainTex.b * generalTransparency); } - diff --git a/shaders/water.vs b/shaders/water.vs index a8ba9ce..dc4e72c 100644 --- a/shaders/water.vs +++ b/shaders/water.vs @@ -5,6 +5,7 @@ in vec4 vertexColor; out vec2 fragTexCoord; out vec4 fragColor; +out vec2 fragWorldPos; uniform mat4 mvp; @@ -12,6 +13,6 @@ void main() { fragTexCoord = vertexTexCoord; fragColor = vertexColor; + fragWorldPos = vertexPosition.xy; gl_Position = mvp * vec4(vertexPosition, 1.0); } - diff --git a/src/app.c b/src/app.c index df5d782..70f201b 100644 --- a/src/app.c +++ b/src/app.c @@ -16,36 +16,42 @@ void InitApp(App *app) { InitLogo(&app->logo); InitMenu(&app->menu); InitGame(app); + InitGameOver(&app->gameOver); } void UpdateApp(App *app) { - app->state.running = true; app->state.gameStarted = false; while (app->state.running) { switch (app->state.currentView) { - case LOGO: { - UpdateLogo(&app->logo, &app->state.currentView, &app->state.running); - RenderLogo(&app->logo, &app->flags); - } break; - case MENU: { - UpdateMenu(&app->menu, &app->state, &app->flags); - RenderMenu(&app->menu, &app->font, app->window.title, &app->flags); - } break; - case GAMEPLAY: { - UpdateGame(app); - RenderGame(app); - } break; - default: - break; + case LOGO: { + UpdateLogo(&app->logo, &app->state.currentView, &app->state.running); + RenderLogo(&app->logo, &app->flags); + break; + } + case MENU: { + UpdateMenu(&app->menu, &app->state, &app->flags); + RenderMenu(&app->menu, &app->font, app->window.title, &app->flags, &app->state); + break; + } + case GAMEPLAY: { + UpdateGame(app); + RenderGame(app); + break; + } + case GAMEOVER: { + UpdateGameOver(app); + RenderGameOver(app); + break; + } } } } void CleanupApp(App *app) { - CleanupMenu(&app->menu); CleanupGame(app); + CleanupMenu(&app->menu); CleanupFont(&app->font); CleanUpConfig(&app->config); } diff --git a/src/appGame.c b/src/appGame.c index b3a6d82..460fea4 100644 --- a/src/appGame.c +++ b/src/appGame.c @@ -1,15 +1,18 @@ +#include + #include "app.h" +#include "enginePostfx.h" +#include "game.h" +#include "raylib.h" const ShaderAsset shaderAssets[SHADER_COUNT] = {{"shaders/postfx.fs"}}; -const SoundtrackAsset soundtrackAssets[SOUNDTRACK_COUNT] = { - {"assets/gameplay_theme.mp3"}, {"assets/sea.wav"}}; +const SoundtrackAsset soundtrackAssets[SOUNDTRACK_COUNT] = {{"assets/gameplay_theme.mp3"}, + {"assets/sea.wav"}}; void InitGame(App *app) { - - SetRandomSeed(1234); + SetRandomSeed(GetRandomValue(0, 10000000)); GameState *g = &app->game; - g->assets = (Assets){.shaderAssets = shaderAssets, - .soundtrackAssets = soundtrackAssets}; + g->assets = (Assets){.shaderAssets = shaderAssets, .soundtrackAssets = soundtrackAssets}; for (int i = 0; i < SHADER_COUNT; i++) { g->postFX[i] = CreatePostFX(g->assets.shaderAssets[i].path); @@ -20,68 +23,116 @@ void InitGame(App *app) { } g->target = LoadRenderTexture(app->window.width, app->window.height); + g->postFXTarget = LoadRenderTexture(app->window.width, app->window.height); - CreatePlayer(); - Player *player = GetPlayer(); - CreateCamera(player->physics.position, 10.0f); - g->birds = CreateBirds(); - g->enemies = CreateEnemies(); - g->islands = CreateIslands(); + g->camera = CreateCamera((Vector2){0.0f, 0.0f}, 10.0f); + g->terrain = CreateTerrain(&g->camera); + g->player = CreatePlayer(&g->terrain); + g->camera.target = g->player.physics.position; + g->birds = CreateBirds(&g->camera); + g->enemies = CreateEnemies(&g->camera, &g->terrain); g->floor = CreateFloor(); + g->clouds = CreateClouds(); + g->wake = CreateWake(); g->projectiles = CreateProjectiles(); + g->overlay = CreateOverlay(); - Texture2D mouseTexture = LoadTexture("assets/mouse.png"); + Texture2D mouseTexture = LoadTextureCached("assets/mouse.png"); g->mouse = CreateMouse(0.10f, 5.0f, 10.0f, &mouseTexture); } -void StartGame(App *app) { - if (!app->state.gameStarted) { - app->state.gameStarted = true; - } +static void StartGame(App *app) { + app->state.gameStarted = true; } void UpdateGame(App *app) { GameState *g = &app->game; - if (app->flags.limitFPS) { - SetTargetFPS(60); - } else { - SetTargetFPS(0); - } + SetTargetFPS(app->flags.limitFPS ? 60 : 0); StartGame(app); OnEscapeChangeView(app, MENU); OnWindowClosedStoppApp(app); + OnBoolChangeView(g->player.health <= 0, app, GAMEOVER); - for (int i = 0; i < SHADER_COUNT; i++) { - UpdatePostFX(&g->postFX[i]); - } - - UpdateMouse(&g->mouse); - - Player *player = GetPlayer(); - UpdateCamera2D(&player->physics.position, &g->mouse); - UpdateFloor(&g->floor); - UpdatePlayer(); - UpdateBirds(&g->birds); - UpdateEnemies(&g->enemies); - UpdateIslands(&g->islands); + UpdateShaders(g->postFX, SHADER_COUNT); + UpdateMouse(&g->mouse, &g->camera); + UpdateCamera2D(&g->camera, &g->player.physics.position, &g->mouse); + UpdateFloor(&g->floor, &g->camera); + UpdateClouds(&g->clouds, GetFrameTime()); + UpdateWake(&g->wake, GetFrameTime()); + UpdatePlayer(&g->player); + UpdateBirds(&g->birds, &g->camera); + UpdateTerrain(&g->terrain, &g->camera); + UpdateEnemies(&g->enemies, &g->camera, &g->projectiles, &g->player, &g->terrain); UpdateSoundtrack(&g->soundtracks[AMBIENT], app->flags.soundtrackOn); - UpdateProjectiles(&g->projectiles, &g->enemies); - CheckPlayerCollisions(GetPlayer(), &app->game.enemies); - CheckEnemyCollisions(&app->game.enemies); - CheckProjectileCollisions(&app->game.projectiles, &app->game.enemies, - GetPlayer()); + UpdateProjectiles(&g->projectiles, &g->player); + + UpdateOverlay(&g->overlay, &g->player); + + CheckPlayerCollisions(&g->player, &g->enemies); + CheckEnemyCollisions(&g->enemies); + CheckProjectileCollisions(&g->projectiles, &g->enemies, &g->player); + CheckTerrainCollisions(&g->player, &g->enemies, &g->terrain); +} + +void ResetGame(App *app) { + GameState *g = &app->game; + SetRandomSeed(GetRandomValue(0, 10000000)); + ResetPlayer(&g->player, &g->terrain); + g->camera = CreateCamera(g->player.physics.position, 10.0f); + g->enemies = CreateEnemies(&g->camera, &g->terrain); + ResetProjectiles(&g->projectiles); +} + +static void RenderShipWakes(App *app) { + GameState *g = &app->game; + RenderShipWake(&g->wake, &g->player.wakeTrail, GetActualSpeed(&g->player.physics)); + for (int i = 0; i < g->enemies.count; i++) { + if (!g->enemies.alive[i]) + continue; + RenderShipWake(&g->wake, &g->enemies.wakeTrail[i], GetActualSpeed(&g->enemies.physics[i])); + } } void RenderComponents(App *app) { GameState *g = &app->game; RenderFloor(&g->floor); - RenderPlayer(); + RenderTerrain(&g->terrain); + RenderCloudShadows(&g->clouds, &g->camera); + RenderShipWakes(app); + RenderPlayer(&g->player); RenderEnemies(&g->enemies); RenderBirds(&g->birds); - RenderIslands(&g->islands); RenderProjectiles(&g->projectiles); + RenderClouds(&g->clouds, &g->camera); +} + +static void RenderPostFXChain(App *app, RenderTexture2D *sceneTarget) { + GameState *g = &app->game; + RenderTexture2D *buffers[2] = {sceneTarget, &g->postFXTarget}; + int srcIndex = 0; + + for (int i = 0; i < SHADER_COUNT; i++) { + bool isLast = (i == SHADER_COUNT - 1); + RenderTexture2D *source = buffers[srcIndex]; + int dstIndex = 1 - srcIndex; + + if (isLast) { + BeginDrawing(); + ClearBackground(BLACK); + RenderPostFX(&g->postFX[i], source); + if (app->flags.showFPS) { + DrawFPS(10, 10); + } + EndDrawing(); + } else { + BeginTextureMode(*buffers[dstIndex]); + RenderPostFX(&g->postFX[i], source); + EndTextureMode(); + srcIndex = dstIndex; + } + } } void RenderGame(App *app) { @@ -89,24 +140,13 @@ void RenderGame(App *app) { BeginTextureMode(g->target); ClearBackground(BLACK); - Camera2D *camera = GetCamera(); - BeginMode2D(*camera); + BeginMode2D(g->camera); RenderComponents(app); EndMode2D(); - RenderOverlay(&g->mouse); + RenderOverlay(&g->mouse, &g->player, &g->overlay.rose); EndTextureMode(); - BeginDrawing(); - ClearBackground(BLACK); - for (int i = 0; i < SHADER_COUNT; i++) { - RenderPostFX(&g->postFX[i], &g->target); - } - - if (app->flags.showFPS) { - DrawFPS(10, 10); - } - - EndDrawing(); + RenderPostFXChain(app, &g->target); } void CleanupGame(App *app) { @@ -117,5 +157,9 @@ void CleanupGame(App *app) { for (int i = 0; i < SOUNDTRACK_COUNT; i++) { UnloadMusicStream(g->soundtracks[i].music); } + UnloadTerrain(&g->terrain); + UnloadClouds(&g->clouds); + UnloadWake(&g->wake); UnloadRenderTexture(g->target); + UnloadRenderTexture(g->postFXTarget); } diff --git a/src/appGameBirds.c b/src/appGameBirds.c index e9f1699..4aad5dc 100644 --- a/src/appGameBirds.c +++ b/src/appGameBirds.c @@ -1,88 +1,95 @@ -#include "app.h" +#include +#include "birds.h" +#include "engineAssets.h" +#include "engineCamera.h" +#include "enginePhysics.h" +#include "engineRectangle.h" +#include "engineVector.h" const TextureAsset birdsTextureAssets[BIRDS_TEX_COUNT] = { - {"assets/birds.png", 16.0f, 16.0f, 45.0f} -}; + {"assets/birds.png", 16.0f, 16.0f, 45.0f}}; -Birds CreateBirds(void){ - Birds birds = {.count = MAX_BIRDS}; - TextureAsset flyingAsset = birdsTextureAssets[FLYING]; - Texture2D texture = LoadTexture(flyingAsset.path); - float frameWidth = flyingAsset.frameWidth; - float rotation = flyingAsset.rotation; - - for (int i = 0; i < birds.count; i++) - { - birds.sprite[i] = (Sprite){ - .texture = texture, - .frameSize = {frameWidth, frameWidth}, - .sourceRec = {0.0f, 0.0f, frameWidth, frameWidth}, - .destRec = {GetScreenWidth()/2, 0.0f, frameWidth, frameWidth}, - .origin = {frameWidth / 2, frameWidth / 2}, - .rotation = rotation, - .color = WHITE - }; - - birds.animation[i] = (Animation){ - .state = 1, - .currentFrame = 0, - .maxFrame = 2, - .framesCounter = 0, - .framesSpeed = 2.0f, - .animTimer = 0.0f - }; - - Rectangle cameraRec = GetCameraRectangle(); - - birds.physics[i] = (Physics){ - .position = GetPositionOutsideRectangle(cameraRec, 100, 1000), - .direction = (Vector2){0.0f, 0.0f}, - .speed = 5.0f - }; - - birds.arrived[i] = true; - birds.destPos[i] = (Vector2){0.0f, 0.0f}; - - } - - return birds; +static void InitBirdSprite(Birds *birds, int i, Texture2D texture, float frameWidth, + float rotation) { + birds->sprite[i] = (Sprite){.texture = texture, + .frameSize = {frameWidth, frameWidth}, + .sourceRec = {0.0f, 0.0f, frameWidth, frameWidth}, + .destRec = {GetScreenWidth() / 2, 0.0f, frameWidth, frameWidth}, + .origin = {frameWidth / 2, frameWidth / 2}, + .rotation = rotation, + .color = WHITE}; } -void UpdateBirds(Birds *birds){ - Camera2D *camera = GetCamera(); - Rectangle cameraRec = GetCameraRectangle(); +static float InitBirdAnimation(Birds *birds, int i) { + float speedFactor = GetRandomValue(50, 180) / 100.0f; + int framesSpeed = (int)(2.0f * speedFactor + 0.5f); + if (framesSpeed < 1) + framesSpeed = 1; + int maxFrame = 2; - for (int i = 0; i < birds->count; i++) - { - if(birds->arrived[i]) - { - birds->destPos[i] = GetPositionOutsideRectangle(cameraRec, 100, 1000); - birds->arrived[i] = false; - } - if(CheckCollisionPointCircle(birds->physics[i].position, birds->destPos[i], 10.0F)) - { - birds->arrived[i] = true; - } - if(GetDistanceBetweenTwoVectors(birds->physics[i].position, camera->target) > 1000){ - birds->physics[i].position = GetPositionOutsideRectangle(cameraRec, 100, 1000); - } - Vector2 dir = GetDirectionBetweenTwoVectors(birds->physics[i].position, birds->destPos[i]); - UpdatePhysics(&birds->physics[i], dir); - UpdateSpriteDestRec(&birds->sprite[i], &birds->physics[i].position); - float rotation = Vector2ToRotation(dir); - UpdateSpriteRotation(&birds->sprite[i], rotation + 45.0f); - UpdateAnimation(&birds->animation[i], GetFrameTime()); - UpdateSpriteSourceRec(&birds->sprite[i], &(Vector2){birds->sprite[i].frameSize.x * birds->animation[i].currentFrame, birds->sprite[i].frameSize.y}); - } + birds->animation[i] = + (Animation){.state = 1, + .currentFrame = GetRandomValue(0, maxFrame - 1), + .maxFrame = maxFrame, + .framesCounter = 0, + .framesSpeed = framesSpeed, + .animTimer = GetRandomValue(0, 99) / 100.0f * (1.0f / (float)framesSpeed)}; + return speedFactor; } -void RenderBirds(Birds *birds){ +Birds CreateBirds(Camera2D *camera) { + Birds birds = {.count = MAX_BIRDS}; + TextureAsset flyingAsset = birdsTextureAssets[FLYING]; + Texture2D texture = LoadTextureCached(flyingAsset.path); + float frameWidth = flyingAsset.frameWidth; + float rotation = flyingAsset.rotation; - for (int i = 0; i < birds->count; i++) - { - RenderSprite(&birds->sprite[i]); - } + for (int i = 0; i < birds.count; i++) { + InitBirdSprite(&birds, i, texture, frameWidth, rotation); + float speedFactor = InitBirdAnimation(&birds, i); + Rectangle cameraRec = GetCameraRectangle(camera); + birds.physics[i] = (Physics){.position = GetPositionOutsideRectangle(cameraRec, 100, 1000), + .direction = (Vector2){0.0f, 0.0f}, + .speed = 5.0f * speedFactor}; + + birds.arrived[i] = true; + birds.destPos[i] = (Vector2){0.0f, 0.0f}; + } + + return birds; } +void UpdateBirds(Birds *birds, Camera2D *camera) { + Rectangle cameraRec = GetCameraRectangle(camera); + + for (int i = 0; i < birds->count; i++) { + if (birds->arrived[i]) { + birds->destPos[i] = GetPositionOutsideRectangle(cameraRec, 100, 1000); + birds->arrived[i] = false; + } + if (CheckCollisionPointCircle(birds->physics[i].position, birds->destPos[i], 10.0f)) { + birds->arrived[i] = true; + } + if (GetDistanceBetweenTwoVectors(birds->physics[i].position, camera->target) > 1000) { + birds->physics[i].position = GetPositionOutsideRectangle(cameraRec, 100, 1000); + } + + Vector2 dir = GetDirectionBetweenTwoVectors(birds->physics[i].position, birds->destPos[i]); + UpdatePhysics(&birds->physics[i], dir); + UpdateSpriteDestRec(&birds->sprite[i], &birds->physics[i].position); + float rotation = Vector2ToRotation(dir); + UpdateSpriteRotation(&birds->sprite[i], rotation + 45.0f); + UpdateAnimation(&birds->animation[i], GetFrameTime()); + UpdateSpriteSourceRec(&birds->sprite[i], &(Vector2){birds->sprite[i].frameSize.x * + birds->animation[i].currentFrame, + birds->sprite[i].frameSize.y}); + } +} + +void RenderBirds(Birds *birds) { + for (int i = 0; i < birds->count; i++) { + RenderSprite(&birds->sprite[i]); + } +} diff --git a/src/appGameClouds.c b/src/appGameClouds.c new file mode 100644 index 0000000..6e871d1 --- /dev/null +++ b/src/appGameClouds.c @@ -0,0 +1,63 @@ +#include "clouds.h" +#include "engineCamera.h" +#include "raylib.h" +#include "math.h" + +Clouds CreateClouds(void) { + Clouds clouds = {0}; + clouds.shader = LoadShader("shaders/clouds.vs", "shaders/clouds.fs"); + clouds.time = 0.0f; + clouds.coverage = 1.0f; + return clouds; +} + +void UpdateClouds(Clouds *clouds, float deltaTime) { + clouds->time += deltaTime; + int timeLoc = GetShaderLocation(clouds->shader, "time"); + SetShaderValue(clouds->shader, timeLoc, &clouds->time, SHADER_UNIFORM_FLOAT); + + float clearCoverage = 1.1f; + float overcastCoverage = 0.55f; + float percent = 0.5f + 0.5f * sinf(clouds->time * 0.05f); + clouds->coverage = clearCoverage - percent * (clearCoverage - overcastCoverage); + int coverageLoc = GetShaderLocation(clouds->shader, "coverage"); + SetShaderValue(clouds->shader, coverageLoc, &clouds->coverage, SHADER_UNIFORM_FLOAT); +} + +static Rectangle GetCloudCoverageRect(Camera2D *camera) { + Rectangle rec = GetCameraRectangle(camera); + float margin = 128.0f; + rec.x -= margin; + rec.y -= margin; + rec.width += margin * 2.0f; + rec.height += margin * 2.0f; + return rec; +} + +void RenderCloudShadows(Clouds *clouds, Camera2D *camera) { + int isShadowLoc = GetShaderLocation(clouds->shader, "isShadowPass"); + int isShadow = 1; + SetShaderValue(clouds->shader, isShadowLoc, &isShadow, SHADER_UNIFORM_INT); + + Rectangle rec = GetCloudCoverageRect(camera); + BeginShaderMode(clouds->shader); + BeginBlendMode(BLEND_MULTIPLIED); + DrawRectangleRec(rec, WHITE); + EndBlendMode(); + EndShaderMode(); +} + +void RenderClouds(Clouds *clouds, Camera2D *camera) { + int isShadowLoc = GetShaderLocation(clouds->shader, "isShadowPass"); + int isShadow = 0; + SetShaderValue(clouds->shader, isShadowLoc, &isShadow, SHADER_UNIFORM_INT); + + Rectangle rec = GetCloudCoverageRect(camera); + BeginShaderMode(clouds->shader); + DrawRectangleRec(rec, WHITE); + EndShaderMode(); +} + +void UnloadClouds(Clouds *clouds) { + UnloadShader(clouds->shader); +} diff --git a/src/appGameCollisions.c b/src/appGameCollisions.c index d42b723..782d1ea 100644 --- a/src/appGameCollisions.c +++ b/src/appGameCollisions.c @@ -1,102 +1,176 @@ -#include "app.h" +#include +#include + +#include "collisions.h" +#include "enginePhysics.h" +#include "engineVector.h" float playerCollisionRadius = 8.0f; float enemiesCollisionRadius = 8.0f; float projectilesCollisionRadius = 1.0f; +static void PlayImpactSound(Sound sound) { + SetSoundPitch(sound, (float)GetRandomValue(90, 110) / 100.0f); + SetSoundVolume(sound, (float)GetRandomValue(85, 100) / 100.0f); + PlaySound(sound); +} + void CheckPlayerCollisions(Player *player, Enemies *enemies) { for (int i = 0; i < enemies->count; i++) { + if (!enemies->alive[i]) + continue; Vector2 playerPos = player->physics.position; Vector2 enemiesPos = enemies->physics[i].position; - float minDist = playerCollisionRadius + enemiesCollisionRadius; + CircleCollision collision = + CheckCircleCollision(playerPos, playerCollisionRadius, enemiesPos, enemiesCollisionRadius); - if (CheckCollisionCircles(playerPos, playerCollisionRadius, enemiesPos, - enemiesCollisionRadius)) { + if (!collision.overlapping) + continue; - player->health -= 1; + player->health -= 1; + if (player->health < 0) + player->health = 0; - float dist = GetDistanceBetweenTwoVectors(playerPos, enemiesPos); - float overlap = minDist - dist; - - if (dist > 0.0f) { - Vector2 collisionVector = GetDirectionBetweenTwoVectors( - enemies->physics[i].position, player->physics.position); - player->physics.position.x += collisionVector.x * overlap; - player->physics.position.y += collisionVector.y * overlap; - Vector2 forward = RotationToVector2(player->rotation - 90.0f); - float side = Cross2D(forward, collisionVector); - player->rotation += side * 120.0f * GetFrameTime(); - } + if (collision.hasDirection) { + player->physics.position.x += collision.direction.x * collision.overlap; + player->physics.position.y += collision.direction.y * collision.overlap; + Vector2 forward = RotationToVector2(player->rotation - 90.0f); + float side = Cross2D(forward, collision.direction); + ApplyCollisionSteer(&player->rotation, side, GetFrameTime()); } } } void CheckEnemyCollisions(Enemies *enemies) { for (int a = 0; a < enemies->count; a++) { + if (!enemies->alive[a]) + continue; + for (int b = a + 1; b < enemies->count; b++) { + if (!enemies->alive[b]) + continue; Vector2 posA = enemies->physics[a].position; Vector2 posB = enemies->physics[b].position; - float minDist = enemiesCollisionRadius * 2; + CircleCollision collision = + CheckCircleCollision(posA, enemiesCollisionRadius, posB, enemiesCollisionRadius); - if (CheckCollisionCircles(posA, enemiesCollisionRadius, posB, - enemiesCollisionRadius)) { + if (!collision.overlapping || !collision.hasDirection) + continue; - Vector2 collisionVectorA = GetDirectionBetweenTwoVectors(posB, posA); - Vector2 collisionVectorB = GetDirectionBetweenTwoVectors(posA, posB); - float dist = GetDistanceBetweenTwoVectors(posA, posB); - float overlap = minDist - dist; + enemies->physics[a].position.x += collision.direction.x * collision.overlap * 0.5f; + enemies->physics[a].position.y += collision.direction.y * collision.overlap * 0.5f; - if (dist > 0.0f) { - enemies->physics[a].position.x += collisionVectorA.x * overlap * 0.5f; - enemies->physics[a].position.y += collisionVectorA.y * overlap * 0.5f; + enemies->physics[b].position.x -= collision.direction.x * collision.overlap * 0.5f; + enemies->physics[b].position.y -= collision.direction.y * collision.overlap * 0.5f; - enemies->physics[b].position.x += collisionVectorB.x * overlap * 0.5f; - enemies->physics[b].position.y += collisionVectorB.y * overlap * 0.5f; - - Vector2 forwardA = - RotationToVector2(enemies->sprite[a].rotation - 90.0f); - Vector2 forwardB = - RotationToVector2(enemies->sprite[b].rotation - 90.0f); - float sideA = Cross2D(forwardA, collisionVectorA); - float sideB = Cross2D(forwardB, collisionVectorB); - enemies->sprite[a].rotation += sideA * 120.0f * GetFrameTime(); - enemies->sprite[b].rotation += sideB * 120.0f * GetFrameTime(); - } - } + Vector2 forwardA = RotationToVector2(enemies->sprite[a].rotation - 90.0f); + Vector2 forwardB = RotationToVector2(enemies->sprite[b].rotation - 90.0f); + Vector2 collisionVectorB = {-collision.direction.x, -collision.direction.y}; + float sideA = Cross2D(forwardA, collision.direction); + float sideB = Cross2D(forwardB, collisionVectorB); + ApplyCollisionSteer(&enemies->sprite[a].rotation, sideA, GetFrameTime()); + ApplyCollisionSteer(&enemies->sprite[b].rotation, sideB, GetFrameTime()); } } } -void CheckProjectileCollisions(Projectiles *projectiles, Enemies *enemies, - Player *player) { +void CheckProjectileCollisions(Projectiles *projectiles, Enemies *enemies, Player *player) { for (int i = 0; i < projectiles->count; i++) { if (!projectiles->active[i]) continue; Vector2 projectilePos = projectiles->physics[i].position; + for (int j = 0; j < enemies->count; j++) { + if (!enemies->alive[j]) + continue; Vector2 enemyPos = enemies->physics[j].position; - if (CheckCollisionCircles(projectilePos, projectilesCollisionRadius, - enemyPos, enemiesCollisionRadius)) { - player->gold += 20; - Rectangle cameraRec = GetCameraRectangle(); - enemies->physics[j].position = - GetPositionOutsideRectangle(cameraRec, 100, 1000); + if (CheckCollisionCircles(projectilePos, projectilesCollisionRadius, enemyPos, + enemiesCollisionRadius)) { + PlayImpactSound(projectiles->soundImpact[i]); + enemies->alive[j] = false; + projectiles->active[i] = false; + if (projectiles->friendly[i]) { + player->gold += 20; + SavePlayer(player); + } + break; } } if (!projectiles->active[i]) continue; - Vector2 playerPos = player->physics.position; - if (CheckCollisionCircles(projectilePos, projectilesCollisionRadius, - playerPos, playerCollisionRadius)) { + if (!projectiles->friendly[i]) { + Vector2 playerPos = player->physics.position; + if (CheckCollisionCircles(projectilePos, projectilesCollisionRadius, playerPos, + playerCollisionRadius)) { + PlayImpactSound(projectiles->soundImpact[i]); + player->health -= 20; + if (player->health < 0) + player->health = 0; + projectiles->active[i] = false; + } + } + } +} - player->health -= 20; +#define TERRAIN_COLLISION_SAMPLES 16 + +static bool ResolveTerrainCollision(Terrain *terrain, Vector2 *position, float radius, + Vector2 *outPushDirection) { + Vector2 pushSum = {0.0f, 0.0f}; + int hits = 0; + + for (int i = 0; i < TERRAIN_COLLISION_SAMPLES; i++) { + float angle = (2.0f * PI * (float)i) / (float)TERRAIN_COLLISION_SAMPLES; + Vector2 direction = {cosf(angle), sinf(angle)}; + Vector2 samplePos = {position->x + direction.x * radius, position->y + direction.y * radius}; + + if (IsTerrainLand(terrain, samplePos)) { + pushSum.x -= direction.x; + pushSum.y -= direction.y; + hits++; + } + } + + if (hits == 0) + return false; + + float length = sqrtf(pushSum.x * pushSum.x + pushSum.y * pushSum.y); + if (length < 0.0001f) + return false; + + Vector2 pushDirection = {pushSum.x / length, pushSum.y / length}; + float penetration = radius * ((float)hits / (float)TERRAIN_COLLISION_SAMPLES); + position->x += pushDirection.x * penetration; + position->y += pushDirection.y * penetration; + *outPushDirection = pushDirection; + return true; +} + +void CheckTerrainCollisions(Player *player, Enemies *enemies, Terrain *terrain) { + Vector2 pushDirection; + if (ResolveTerrainCollision(terrain, &player->physics.position, playerCollisionRadius, + &pushDirection)) { + Vector2 forward = RotationToVector2(player->rotation - 90.0f); + float side = Cross2D(forward, pushDirection); + ApplyCollisionSteer(&player->rotation, side, GetFrameTime()); + } + + for (int j = 0; j < enemies->count; j++) { + if (!enemies->alive[j]) + continue; + + if (ResolveTerrainCollision(terrain, &enemies->physics[j].position, enemiesCollisionRadius, + &pushDirection)) { + Vector2 forward = RotationToVector2(enemies->sprite[j].rotation - 90.0f); + float side = Cross2D(forward, pushDirection); + ApplyCollisionSteer(&enemies->sprite[j].rotation, side, GetFrameTime()); } } } diff --git a/src/appGameEnemies.c b/src/appGameEnemies.c index 88f51d3..1339e5f 100644 --- a/src/appGameEnemies.c +++ b/src/appGameEnemies.c @@ -1,24 +1,248 @@ -#include "app.h" +#include +#include + +#include "enemies.h" +#include "engineAssets.h" +#include "engineCamera.h" +#include "enginePhysics.h" +#include "engineRectangle.h" +#include "engineVector.h" +#include "pathfinding.h" const TextureAsset enemiesTextureAssets[ENEMIES_TEX_COUNT] = { {"assets/ship_start.png", 16.0f, 16.0f, 90.0f}}; -Enemies CreateEnemies(void) { +#define ENEMY_DETECTION_RANGE 70.0f +#define ENEMY_ROTATION_SPEED 50.0f +#define ENEMY_AGGRO_RANGE 250.0f +#define ENEMY_CHASE_REPATH_INTERVAL 1.0f +#define ENEMY_HIT_TOLERANCE 16.0f +#define ENEMY_RELOAD_TIME 1.5f +#define ENEMY_MUZZLE_OFFSET 12.0f +#define WATER_POSITION_MAX_ATTEMPTS 20 +#define ENEMY_TERRAIN_CLEARANCE 40.0f +#define ENEMY_WATER_SEARCH_MAX_RADIUS 800.0f + +static Vector2 GetRandomClearWaterPosition(Terrain *terrain, Rectangle cameraRec, + Vector2 fallbackOrigin, int min, int max, + float clearance) { + for (int attempt = 0; attempt < WATER_POSITION_MAX_ATTEMPTS; attempt++) { + Vector2 candidate = GetPositionOutsideRectangle(cameraRec, min, max); + if (IsWaterWithClearance(terrain, candidate, clearance)) + return candidate; + } + + return GetRandomWaterPositionNear(terrain, fallbackOrigin, ENEMY_WATER_SEARCH_MAX_RADIUS, + clearance); +} + +#define ENEMY_MIN_SPAWN_SEPARATION 24.0f +#define SPAWN_SEPARATION_MAX_ATTEMPTS 80 + +static bool IsFarFromOtherEnemies(Enemies *enemies, int selfIndex, Vector2 pos) { + for (int k = 0; k < enemies->count; k++) { + if (k == selfIndex || !enemies->alive[k]) + continue; + if (GetDistanceBetweenTwoVectors(pos, enemies->physics[k].position) < + ENEMY_MIN_SPAWN_SEPARATION) { + return false; + } + } + return true; +} + +static Vector2 GetSpawnPosition(Terrain *terrain, Rectangle cameraRec, Vector2 cameraTarget, + Enemies *enemies, int selfIndex) { + Vector2 candidate = {0}; + for (int attempt = 0; attempt < SPAWN_SEPARATION_MAX_ATTEMPTS; attempt++) { + candidate = GetRandomClearWaterPosition(terrain, cameraRec, cameraTarget, 100, 700, + ENEMY_TERRAIN_CLEARANCE); + if (IsFarFromOtherEnemies(enemies, selfIndex, candidate)) + return candidate; + } + return candidate; +} + +#define DESTINATION_MAX_ATTEMPTS 5 + +static void CommitPath(Enemies *enemies, int i, Terrain *terrain, Path path, Vector2 origin) { + if (path.count == 0) { + Rectangle localRec = {origin.x, origin.y, 0.0f, 0.0f}; + Vector2 destination = + GetRandomClearWaterPosition(terrain, localRec, origin, 20, 80, ENEMY_TERRAIN_CLEARANCE); + path = FindWaterPath(terrain, origin, destination, ENEMY_TERRAIN_CLEARANCE); + } + + if (path.count == 0) { + path.waypoints[0] = origin; + path.count = 1; + enemies->destinationCooldown[i] = 2.0f; + } + + for (int w = 0; w < path.count; w++) { + enemies->path[i][w] = path.waypoints[w]; + } + enemies->pathLength[i] = path.count; + enemies->pathIndex[i] = 0; + enemies->destPos[i] = enemies->path[i][0]; + enemies->arrived[i] = false; +} + +static void AssignNewDestination(Enemies *enemies, int i, Terrain *terrain, Rectangle cameraRec, + Vector2 cameraTarget) { + Vector2 origin = enemies->physics[i].position; + Path path = {0}; + + for (int attempt = 0; attempt < DESTINATION_MAX_ATTEMPTS && path.count == 0; attempt++) { + Vector2 destination = GetRandomClearWaterPosition(terrain, cameraRec, cameraTarget, 100, 500, + ENEMY_TERRAIN_CLEARANCE); + path = FindWaterPath(terrain, origin, destination, ENEMY_TERRAIN_CLEARANCE); + } + + CommitPath(enemies, i, terrain, path, origin); +} + +static void AssignChaseDestination(Enemies *enemies, int i, Terrain *terrain, + Vector2 playerPosition) { + Vector2 origin = enemies->physics[i].position; + Path path = FindWaterPath(terrain, origin, playerPosition, ENEMY_TERRAIN_CLEARANCE); + CommitPath(enemies, i, terrain, path, origin); +} + +static void RespawnDeadEnemy(Enemies *enemies, int i, Terrain *terrain, Rectangle cameraRec, + Camera2D *camera) { + if (enemies->alive[i]) + return; + + enemies->physics[i].position = GetSpawnPosition(terrain, cameraRec, camera->target, enemies, i); + enemies->arrived[i] = true; + enemies->alive[i] = true; + enemies->chasing[i] = false; + enemies->reloadTimer[i] = ENEMY_RELOAD_TIME; + ResetWakeTrail(&enemies->wakeTrail[i]); +} + +static void UpdateEnemyTargeting(Enemies *enemies, int i, Terrain *terrain, Rectangle cameraRec, + Camera2D *camera, Vector2 playerPos, float distanceToPlayer) { + if (enemies->destinationCooldown[i] > 0.0f) { + enemies->destinationCooldown[i] -= GetFrameTime(); + } + if (enemies->chaseRepathTimer[i] > 0.0f) { + enemies->chaseRepathTimer[i] -= GetFrameTime(); + } + + bool shouldChase = distanceToPlayer <= ENEMY_AGGRO_RANGE; + if (shouldChase) { + if (!enemies->chasing[i] || enemies->arrived[i] || enemies->chaseRepathTimer[i] <= 0.0f) { + AssignChaseDestination(enemies, i, terrain, playerPos); + enemies->chasing[i] = true; + enemies->chaseRepathTimer[i] = ENEMY_CHASE_REPATH_INTERVAL; + } + } else if (enemies->chasing[i]) { + enemies->chasing[i] = false; + enemies->arrived[i] = true; + } + + if (!enemies->chasing[i] && enemies->arrived[i] && enemies->destinationCooldown[i] <= 0.0f) { + AssignNewDestination(enemies, i, terrain, cameraRec, camera->target); + } +} + +static void AdvanceEnemyAlongPath(Enemies *enemies, int i) { + if (!CheckCollisionPointCircle(enemies->physics[i].position, enemies->destPos[i], 10.0f)) { + return; + } + + enemies->pathIndex[i]++; + if (enemies->pathIndex[i] >= enemies->pathLength[i]) { + enemies->arrived[i] = true; + } else { + enemies->destPos[i] = enemies->path[i][enemies->pathIndex[i]]; + } +} + +static void RecycleFarEnemy(Enemies *enemies, int i, Terrain *terrain, Rectangle cameraRec, + Camera2D *camera) { + if (GetDistanceBetweenTwoVectors(enemies->physics[i].position, camera->target) <= 1000) + return; + + enemies->physics[i].position = GetSpawnPosition(terrain, cameraRec, camera->target, enemies, i); + enemies->arrived[i] = true; + enemies->chasing[i] = false; + ResetWakeTrail(&enemies->wakeTrail[i]); +} + +static void SteerAndMoveEnemy(Enemies *enemies, int i) { + Vector2 idealDir = + GetDirectionBetweenTwoVectors(enemies->physics[i].position, enemies->destPos[i]); + float targetRotation = Vector2ToRotation(idealDir) + 90.0f; + UpdateSpriteRotationTowards(&enemies->sprite[i], targetRotation, ENEMY_ROTATION_SPEED, + GetFrameTime()); + + Vector2 actualDir = RotationToVector2(enemies->sprite[i].rotation - 90.0f); + UpdatePhysics(&enemies->physics[i], actualDir); + UpdateSpriteDestRec(&enemies->sprite[i], &enemies->physics[i].position); + UpdateWakeTrail(&enemies->wakeTrail[i], enemies->physics[i].position); +} + +static void UpdateEnemyAnimationFrame(Enemies *enemies, int i) { + int speedFrame = (enemies->physics[i].speed > 0.0f) ? 1 : 0; + UpdateSpriteSourceRec(&enemies->sprite[i], &(Vector2){enemies->sprite[i].frameSize.x * speedFrame, + enemies->sprite[i].frameSize.y}); +} + +static void TryFireAtPlayer(Enemies *enemies, int i, Projectiles *projectiles, Player *player) { + if (enemies->reloadTimer[i] > 0.0f) { + enemies->reloadTimer[i] -= GetFrameTime(); + } + + Vector2 enemyPos = enemies->physics[i].position; + Vector2 playerPos = player->physics.position; + Vector2 toPlayer = Vector2Subtract(playerPos, enemyPos); + float distanceToPlayer = Vector2Length(toPlayer); + + if (enemies->reloadTimer[i] > 0.0f || distanceToPlayer > ENEMY_DETECTION_RANGE) + return; + + Vector2 portDirection = Vector2Rotate(enemies->physics[i].direction, -90.0f * DEG2RAD); + Vector2 starboardDirection = Vector2Rotate(enemies->physics[i].direction, 90.0f * DEG2RAD); + Vector2 aimDirection = + (Vector2DotProduct(toPlayer, portDirection) > 0.0f) ? portDirection : starboardDirection; + + float travelDistance = Vector2DotProduct(toPlayer, aimDirection); + if (travelDistance <= 0.0f) + return; + + float travelTime = travelDistance / PROJECTILE_SPEED; + Vector2 closestPointOnShot = Vector2Add(enemyPos, Vector2Scale(aimDirection, travelDistance)); + Vector2 predictedPlayerPos = Vector2Add( + playerPos, Vector2Scale(player->physics.direction, player->physics.speed * travelTime)); + float missDistance = Vector2Distance(closestPointOnShot, predictedPlayerPos); + + if (missDistance > ENEMY_HIT_TOLERANCE) + return; + + Vector2 firePosition = Vector2Add(enemyPos, Vector2Scale(aimDirection, ENEMY_MUZZLE_OFFSET)); + if (FireProjectile(projectiles, firePosition, aimDirection, false)) { + enemies->reloadTimer[i] = ENEMY_RELOAD_TIME; + } +} + +Enemies CreateEnemies(Camera2D *camera, Terrain *terrain) { Enemies enemies = {.count = MAX_ENEMIES}; TextureAsset textureAsset = enemiesTextureAssets[BOAT]; - Texture2D texture = LoadTexture(textureAsset.path); + Texture2D texture = LoadTextureCached(textureAsset.path); float frameWidth = textureAsset.frameWidth; float rotation = textureAsset.rotation; for (int i = 0; i < enemies.count; i++) { - enemies.sprite[i] = (Sprite){ - .texture = texture, - .frameSize = {frameWidth, frameWidth}, - .sourceRec = {0.0f, 0.0f, frameWidth, frameWidth}, - .destRec = {GetScreenWidth() / 2, 0.0f, frameWidth, frameWidth}, - .origin = {frameWidth / 2, frameWidth / 2}, - .rotation = rotation, - .color = WHITE}; + enemies.sprite[i] = (Sprite){.texture = texture, + .frameSize = {frameWidth, frameWidth}, + .sourceRec = {0.0f, 0.0f, frameWidth, frameWidth}, + .destRec = {GetScreenWidth() / 2, 0.0f, frameWidth, frameWidth}, + .origin = {frameWidth / 2, frameWidth / 2}, + .rotation = rotation, + .color = WHITE}; enemies.animation[i] = (Animation){.state = 1, .currentFrame = 0, @@ -27,60 +251,44 @@ Enemies CreateEnemies(void) { .framesSpeed = 2.0f, .animTimer = 0.0f}; - Rectangle cameraRec = GetCameraRectangle(); + Rectangle cameraRec = GetCameraRectangle(camera); enemies.physics[i] = - (Physics){.position = GetPositionOutsideRectangle(cameraRec, 100, 1000), + (Physics){.position = GetSpawnPosition(terrain, cameraRec, camera->target, &enemies, i), .direction = (Vector2){0.0f, 0.0f}, .speed = 5.0f, .weight = 5.0f}; enemies.arrived[i] = true; enemies.destPos[i] = (Vector2){0.0f, 0.0f}; + enemies.pathLength[i] = 0; + enemies.pathIndex[i] = 0; + enemies.destinationCooldown[i] = 0.0f; + enemies.chasing[i] = false; + enemies.chaseRepathTimer[i] = 0.0f; enemies.alive[i] = true; + enemies.reloadTimer[i] = GetRandomValue(0, 100) / 100.0f * ENEMY_RELOAD_TIME; } return enemies; } -void UpdateEnemies(Enemies *enemies) { - Camera2D *camera = GetCamera(); - Rectangle cameraRec = GetCameraRectangle(); +void UpdateEnemies(Enemies *enemies, Camera2D *camera, Projectiles *projectiles, Player *player, + Terrain *terrain) { + Rectangle cameraRec = GetCameraRectangle(camera); for (int i = 0; i < enemies->count; i++) { - if (!enemies->alive[i]) - continue; + RespawnDeadEnemy(enemies, i, terrain, cameraRec, camera); - if (enemies->arrived[i]) { - enemies->destPos[i] = GetPositionOutsideRectangle(cameraRec, 100, 1000); - enemies->arrived[i] = false; - } + Vector2 playerPos = player->physics.position; + float distanceToPlayer = GetDistanceBetweenTwoVectors(enemies->physics[i].position, playerPos); - if (CheckCollisionPointCircle(enemies->physics[i].position, - enemies->destPos[i], 10.0F)) { - enemies->arrived[i] = true; - } - - if (GetDistanceBetweenTwoVectors(enemies->physics[i].position, - camera->target) > 1000) { - enemies->physics[i].position = - GetPositionOutsideRectangle(cameraRec, 100, 1000); - } - - Vector2 dir = GetDirectionBetweenTwoVectors(enemies->physics[i].position, - enemies->destPos[i]); - - UpdatePhysics(&enemies->physics[i], dir); - UpdateSpriteDestRec(&enemies->sprite[i], &enemies->physics[i].position); - - float rotation = Vector2ToRotation(dir); - UpdateSpriteRotation(&enemies->sprite[i], rotation + 90.0f); - - UpdateAnimation(&enemies->animation[i], GetFrameTime()); - UpdateSpriteSourceRec(&enemies->sprite[i], - &(Vector2){enemies->sprite[i].frameSize.x * - enemies->animation[i].currentFrame, - enemies->sprite[i].frameSize.y}); + UpdateEnemyTargeting(enemies, i, terrain, cameraRec, camera, playerPos, distanceToPlayer); + AdvanceEnemyAlongPath(enemies, i); + RecycleFarEnemy(enemies, i, terrain, cameraRec, camera); + SteerAndMoveEnemy(enemies, i); + UpdateEnemyAnimationFrame(enemies, i); + TryFireAtPlayer(enemies, i, projectiles, player); } } diff --git a/src/appGameFloor.c b/src/appGameFloor.c index d8a7e60..6563bd9 100644 --- a/src/appGameFloor.c +++ b/src/appGameFloor.c @@ -1,91 +1,141 @@ -#include "app.h" +#include +#include -Floor CreateFloor(void) -{ +#include "engineAssets.h" +#include "floor.h" - Texture2D base = LoadTexture("assets/water_base.png"); - Texture2D caustic = LoadTexture("assets/water_caustics.png"); - Texture2D causticHighlight = LoadTexture("assets/water_caustics_highlights.png"); - Texture2D noise = LoadTexture("assets/water_noise.png"); - Texture2D cloud = LoadTexture("assets/water_cloudnoise.png"); - Texture2D voronoise = LoadTexture("assets/water_voronoise.png"); +static void BindWaterTexture(Shader shader, const char *uniformName, Texture2D texture, int slot) { + int loc = GetShaderLocation(shader, uniformName); + if (loc < 0) + return; - float textureWidth = 64.0f; - - Floor floor = { - .sprite = { - .texture = base, - .frameSize = {textureWidth, textureWidth}, - .sourceRec = {0.0f, 0.0f, textureWidth, textureWidth}, - .destRec = {0.0f, 0.0f, textureWidth, textureWidth}, - .origin = {textureWidth / 2, textureWidth / 2}, - .rotation = 0.0f, - .color = WHITE}, - .bounds = {0}, - .shader = LoadShader("shaders/water.vs", "shaders/water.fs"), - .time = 0.0f - }; - - float worldTileSize = 128.0f; - float textureSize = 32.0f; - - floor.sprite.frameSize = (Vector2){worldTileSize, worldTileSize}; - floor.sprite.sourceRec = (Rectangle){0, 0, textureSize, textureSize}; - floor.sprite.destRec = (Rectangle){0, 0, worldTileSize, worldTileSize}; - floor.sprite.origin = (Vector2){0, 0}; - - SetShaderValueTexture(floor.shader, GetShaderLocation(floor.shader, "causticTexture"), caustic); - SetShaderValueTexture(floor.shader, GetShaderLocation(floor.shader, "causticHighlightTexture"), causticHighlight); - SetShaderValueTexture(floor.shader, GetShaderLocation(floor.shader, "causticNoiseTexture"), noise); - SetShaderValueTexture(floor.shader, GetShaderLocation(floor.shader, "causticFadeNoiseTexture"), cloud); - SetShaderValueTexture(floor.shader, GetShaderLocation(floor.shader, "specularNoiseTexture"), voronoise); - SetShaderValueTexture(floor.shader, GetShaderLocation(floor.shader, "specularMovementLeftNoiseTexture"), voronoise); - SetShaderValueTexture(floor.shader, GetShaderLocation(floor.shader, "specularMovementRightNoiseTexture"), cloud); - - float aspect = (float)GetScreenHeight() / (float)GetScreenWidth(); - float pixelization = 2048.0f; - int aspectLoc = GetShaderLocation(floor.shader, "aspectRatio"); - int pixelLoc = GetShaderLocation(floor.shader, "pixelization"); - SetShaderValue(floor.shader, aspectLoc, &aspect, SHADER_UNIFORM_FLOAT); - SetShaderValue(floor.shader, pixelLoc, &pixelization, SHADER_UNIFORM_FLOAT); - - floor.sprite.texture = base; - - return floor; + rlActiveTextureSlot(slot); + rlEnableTexture(texture.id); + SetShaderValue(shader, loc, &slot, SHADER_UNIFORM_INT); } -void UpdateFloor(Floor *floor) -{ - Camera2D *camera = GetCamera(); - floor->bounds.x = (int)((camera->target.x - (GetScreenWidth() / 2.0f / camera->zoom)) / floor->sprite.frameSize.x) - 1; - floor->bounds.width = (int)((camera->target.x + (GetScreenWidth() / 2.0f / camera->zoom)) / floor->sprite.frameSize.x) + 1; - floor->bounds.y = (int)((camera->target.y - (GetScreenHeight() / 2.0f / camera->zoom)) / floor->sprite.frameSize.y) - 1; - floor->bounds.height = (int)((camera->target.y + (GetScreenHeight() / 2.0f / camera->zoom)) / floor->sprite.frameSize.y) + 1; - - floor->time += GetFrameTime(); - int timeLoc = GetShaderLocation(floor->shader, "time"); - SetShaderValue(floor->shader, timeLoc, &floor->time, SHADER_UNIFORM_FLOAT); +static void PrepareTiledTexture(Texture2D texture) { + SetTextureWrap(texture, TEXTURE_WRAP_REPEAT); + GenTextureMipmaps(&texture); + SetTextureFilter(texture, TEXTURE_FILTER_TRILINEAR); } -void RenderFloor(Floor *floor) -{ - BeginShaderMode(floor->shader); +static Texture2D CreateDepthGradientTexture(void) { + Image gradientImage = + GenImageGradientLinear(64, 64, 90, (Color){12, 44, 84, 255}, (Color){64, 168, 186, 255}); + Texture2D depthGradient = LoadTextureFromImage(gradientImage); + UnloadImage(gradientImage); + SetTextureWrap(depthGradient, TEXTURE_WRAP_CLAMP); + return depthGradient; +} - for (int y = (int)floor->bounds.y; y <= (int)floor->bounds.height; y++) - { - for (int x = (int)floor->bounds.x; x <= (int)floor->bounds.width; x++) - { - Vector2 floorPosition = {x * floor->sprite.frameSize.x, y * floor->sprite.frameSize.y}; - UpdateSpriteDestRec(&floor->sprite, &floorPosition); - RenderSprite(&floor->sprite); - } +static void ConfigureFloorShaderUniforms(Shader shader, Texture2D base, float worldTileSize, + float textureSize) { + float aspect = (float)GetScreenHeight() / (float)GetScreenWidth(); + float pixelization = 2048.0f; + float noiseCoordinateScale = worldTileSize * ((float)base.width / textureSize); + + int aspectLoc = GetShaderLocation(shader, "aspectRatio"); + int pixelLoc = GetShaderLocation(shader, "pixelization"); + int noiseCoordinateScaleLoc = GetShaderLocation(shader, "noiseCoordinateScale"); + SetShaderValue(shader, aspectLoc, &aspect, SHADER_UNIFORM_FLOAT); + SetShaderValue(shader, pixelLoc, &pixelization, SHADER_UNIFORM_FLOAT); + SetShaderValue(shader, noiseCoordinateScaleLoc, &noiseCoordinateScale, SHADER_UNIFORM_FLOAT); +} + +Floor CreateFloor(void) { + Texture2D base = LoadTextureCached("assets/water_base.png"); + Texture2D caustic = LoadTextureCached("assets/water_caustics.png"); + Texture2D causticHighlight = LoadTextureCached("assets/water_caustics_highlights.png"); + Texture2D noise = LoadTextureCached("assets/water_noise.png"); + Texture2D cloud = LoadTextureCached("assets/water_cloudnoise.png"); + Texture2D voronoise = LoadTextureCached("assets/water_voronoise.png"); + + PrepareTiledTexture(caustic); + PrepareTiledTexture(causticHighlight); + PrepareTiledTexture(noise); + PrepareTiledTexture(cloud); + PrepareTiledTexture(voronoise); + + Texture2D depthGradient = CreateDepthGradientTexture(); + + float textureWidth = 64.0f; + Floor floor = {.sprite = {.texture = base, + .frameSize = {textureWidth, textureWidth}, + .sourceRec = {0.0f, 0.0f, textureWidth, textureWidth}, + .destRec = {0.0f, 0.0f, textureWidth, textureWidth}, + .origin = {textureWidth / 2, textureWidth / 2}, + .rotation = 0.0f, + .color = WHITE}, + .bounds = {0}, + .shader = LoadShader("shaders/water.vs", "shaders/water.fs"), + .time = 0.0f}; + + float worldTileSize = 128.0f; + float textureSize = 32.0f; + + floor.sprite.frameSize = (Vector2){worldTileSize, worldTileSize}; + floor.sprite.sourceRec = (Rectangle){0, 0, textureSize, textureSize}; + floor.sprite.destRec = (Rectangle){0, 0, worldTileSize, worldTileSize}; + floor.sprite.origin = (Vector2){0, 0}; + + ConfigureFloorShaderUniforms(floor.shader, base, worldTileSize, textureSize); + + floor.sprite.texture = base; + floor.causticTexture = caustic; + floor.causticHighlightTexture = causticHighlight; + floor.noiseTexture = noise; + floor.cloudTexture = cloud; + floor.voronoiseTexture = voronoise; + floor.depthGradientTexture = depthGradient; + + return floor; +} + +void UpdateFloor(Floor *floor, Camera2D *camera) { + floor->bounds.x = (int)((camera->target.x - (GetScreenWidth() / 2.0f / camera->zoom)) / + floor->sprite.frameSize.x) - + 1; + floor->bounds.width = (int)((camera->target.x + (GetScreenWidth() / 2.0f / camera->zoom)) / + floor->sprite.frameSize.x) + + 1; + floor->bounds.y = (int)((camera->target.y - (GetScreenHeight() / 2.0f / camera->zoom)) / + floor->sprite.frameSize.y) - + 1; + floor->bounds.height = (int)((camera->target.y + (GetScreenHeight() / 2.0f / camera->zoom)) / + floor->sprite.frameSize.y) + + 1; + + floor->time += GetFrameTime(); + int timeLoc = GetShaderLocation(floor->shader, "time"); + SetShaderValue(floor->shader, timeLoc, &floor->time, SHADER_UNIFORM_FLOAT); +} + +void RenderFloor(Floor *floor) { + BeginShaderMode(floor->shader); + + BindWaterTexture(floor->shader, "causticTexture", floor->causticTexture, 1); + BindWaterTexture(floor->shader, "causticHighlightTexture", floor->causticHighlightTexture, 2); + BindWaterTexture(floor->shader, "causticNoiseTexture", floor->noiseTexture, 3); + BindWaterTexture(floor->shader, "causticFadeNoiseTexture", floor->cloudTexture, 4); + BindWaterTexture(floor->shader, "specularNoiseTexture", floor->voronoiseTexture, 5); + BindWaterTexture(floor->shader, "specularMovementLeftNoiseTexture", floor->voronoiseTexture, 6); + BindWaterTexture(floor->shader, "specularMovementRightNoiseTexture", floor->cloudTexture, 7); + BindWaterTexture(floor->shader, "foamTexture", floor->voronoiseTexture, 8); + BindWaterTexture(floor->shader, "waterDepthGradient", floor->depthGradientTexture, 9); + + for (int y = (int)floor->bounds.y; y <= (int)floor->bounds.height; y++) { + for (int x = (int)floor->bounds.x; x <= (int)floor->bounds.width; x++) { + Vector2 floorPosition = {x * floor->sprite.frameSize.x, y * floor->sprite.frameSize.y}; + UpdateSpriteDestRec(&floor->sprite, &floorPosition); + RenderSprite(&floor->sprite); } + } - EndShaderMode(); + EndShaderMode(); } -void UnloadFloor(Floor *floor) -{ - UnloadShader(floor->shader); +void UnloadFloor(Floor *floor) { + UnloadShader(floor->shader); + UnloadTexture(floor->depthGradientTexture); } - diff --git a/src/appGameIslands.c b/src/appGameIslands.c index 172b21f..3615b1c 100644 --- a/src/appGameIslands.c +++ b/src/appGameIslands.c @@ -1,39 +1,199 @@ -#include "app.h" +#include "islands.h" +#include "engineCamera.h" +#include "enginePhysics.h" +#include "engineRectangle.h" +#include "engineVector.h" +#include +#include +#include -const TextureAsset islandsTextureAssets[ISLANDS_TEX_COUNT] = { - {"assets/island.png", 16.0f, 16.0f, 0.0f}}; +// How far an island can drift from the camera before it's considered "left +// behind" and gets regenerated fresh outside the current view, the same +// recycling idea used for enemies/birds so the world stays populated as the +// player explores. +#define ISLAND_RECYCLE_DISTANCE 800.0f +#define ISLAND_MIN_SPAWN_DISTANCE 80 +#define ISLAND_MAX_SPAWN_DISTANCE 600 +// Concentric elevation bands, each a fraction of the coastline radius at +// that angle — same idea as a real elevation-banded map (shore/lowland/ +// highland rings), just three tiers instead of five since this renders at +// pixel-art scale, not map scale. +#define ISLAND_GRASS_SCALE 0.72f +#define ISLAND_HILL_SCALE 0.35f +// Native resolution the shape is painted at. Higher than the 16x16 ship/bird +// sprites — islands are the biggest thing on screen now, and matching their +// texel density would make each "pixel" a huge, coarse block; this keeps +// the pixel-art look while the individual pixels stay small relative to the +// island's size. +#define ISLAND_IMAGE_SIZE 128 -Islands CreateIslands(void) { - TextureAsset islandAsset = islandsTextureAssets[ISLAND]; - Texture2D texture = LoadTexture(islandAsset.path); - float frameWidth = islandAsset.frameWidth; - float rotation = islandAsset.rotation; +// A coastline is two overlapping cosine lobes (a broad one for bays/ +// peninsulas, a finer one for coastal roughness) plus light jitter, instead +// of a single jittered circle — the same kind of shape irregularity a real +// procedural terrain generator uses, just far cheaper. +static void GenerateCoastline(float shoreRadiusPx[ISLAND_SHAPE_POINTS], float baseRadius) { + float amp1 = (float)GetRandomValue(15, 30) / 100.0f; + float freq1 = (float)GetRandomValue(2, 3); + float phase1 = (float)GetRandomValue(0, 628) / 100.0f; + float amp2 = (float)GetRandomValue(5, 15) / 100.0f; + float freq2 = (float)GetRandomValue(5, 8); + float phase2 = (float)GetRandomValue(0, 628) / 100.0f; - Sprite islandSprite = { + for (int i = 0; i < ISLAND_SHAPE_POINTS; i++) { + float angle = (2.0f * PI * (float)i) / (float)ISLAND_SHAPE_POINTS; + float shapeFactor = 1.0f + + amp1 * cosf(freq1 * angle + phase1) + + amp2 * cosf(freq2 * angle + phase2); + if (shapeFactor < 0.5f) shapeFactor = 0.5f; + + float jitter = (float)GetRandomValue(90, 110) / 100.0f; + shoreRadiusPx[i] = baseRadius * shapeFactor * jitter; + } + + // Average each radius with its neighbours a couple of times so the + // outline reads as an irregular but rounded coastline instead of a spiky + // star. + for (int pass = 0; pass < 2; pass++) { + float smoothed[ISLAND_SHAPE_POINTS]; + for (int i = 0; i < ISLAND_SHAPE_POINTS; i++) { + int prev = (i - 1 + ISLAND_SHAPE_POINTS) % ISLAND_SHAPE_POINTS; + int next = (i + 1) % ISLAND_SHAPE_POINTS; + smoothed[i] = (shoreRadiusPx[prev] + shoreRadiusPx[i] + shoreRadiusPx[next]) / 3.0f; + } + for (int i = 0; i < ISLAND_SHAPE_POINTS; i++) shoreRadiusPx[i] = smoothed[i]; + } +} + +static float SampleRadius(const float shoreRadiusPx[ISLAND_SHAPE_POINTS], float angle) { + float t = angle / (2.0f * PI) * ISLAND_SHAPE_POINTS; + int i0 = ((int)t) % ISLAND_SHAPE_POINTS; + int i1 = (i0 + 1) % ISLAND_SHAPE_POINTS; + float frac = t - (int)t; + return shoreRadiusPx[i0] * (1.0f - frac) + shoreRadiusPx[i1] * frac; +} + +static Texture2D GenerateIslandTexture(const float shoreRadiusPx[ISLAND_SHAPE_POINTS], + Color shoreColor, Color grassColor, Color hillColor) { + Image image = GenImageColor(ISLAND_IMAGE_SIZE, ISLAND_IMAGE_SIZE, BLANK); + float center = ISLAND_IMAGE_SIZE / 2.0f; + + for (int y = 0; y < ISLAND_IMAGE_SIZE; y++) { + for (int x = 0; x < ISLAND_IMAGE_SIZE; x++) { + float dx = (x + 0.5f) - center; + float dy = (y + 0.5f) - center; + float dist = sqrtf(dx * dx + dy * dy); + + float angle = atan2f(dy, dx); + if (angle < 0.0f) angle += 2.0f * PI; + + float shoreRadius = SampleRadius(shoreRadiusPx, angle); + + if (dist <= shoreRadius * ISLAND_HILL_SCALE) { + ImageDrawPixel(&image, x, y, hillColor); + } else if (dist <= shoreRadius * ISLAND_GRASS_SCALE) { + ImageDrawPixel(&image, x, y, grassColor); + } else if (dist <= shoreRadius) { + ImageDrawPixel(&image, x, y, shoreColor); + } + } + } + + Texture2D texture = LoadTextureFromImage(image); + UnloadImage(image); + SetTextureFilter(texture, TEXTURE_FILTER_POINT); + return texture; +} + +static void GenerateIsland(Island *island, Vector2 position) { + if (island->sprite.texture.id != 0) { + UnloadTexture(island->sprite.texture); + } + + island->position = position; + + float worldDiameter = (float)GetRandomValue(220, 380); + + float basePixelRadius = (float)GetRandomValue(30, 44); + float shoreRadiusPx[ISLAND_SHAPE_POINTS]; + GenerateCoastline(shoreRadiusPx, basePixelRadius); + + // The collision radius has to track the coastline's actual average + // extent, not a fraction of the destRec/canvas size — the canvas has a + // fair amount of open water margin around the shore (needed so the + // harmonic wobble and jitter don't clip off-canvas), so anything based on + // worldDiameter directly ends up much bigger than the visible island, + // making the ship "bump" into open water well before the shore. + float avgRadiusPx = 0.0f; + for (int i = 0; i < ISLAND_SHAPE_POINTS; i++) avgRadiusPx += shoreRadiusPx[i]; + avgRadiusPx /= ISLAND_SHAPE_POINTS; + float pixelToWorld = worldDiameter / (float)ISLAND_IMAGE_SIZE; + // Slightly over the average so the collision circle covers the coastline + // at most angles instead of sitting exactly on it — the shape bulges out + // past the average at peninsulas, and a ship visually overlapping the + // shore reads worse than the collision boundary having a little slack. + island->radius = avgRadiusPx * pixelToWorld * 1.05f; + + // Slight per-island tint variation so a cluster of islands doesn't look + // like the same stamp copy-pasted around. + Color shoreColor = (Color){ + (unsigned char)Clamp(233 + GetRandomValue(-10, 10), 0, 255), + (unsigned char)Clamp(220 + GetRandomValue(-10, 10), 0, 255), + (unsigned char)Clamp(178 + GetRandomValue(-10, 10), 0, 255), + 255}; + Color grassColor = (Color){ + (unsigned char)Clamp(80 + GetRandomValue(-15, 15), 0, 255), + (unsigned char)Clamp(168 + GetRandomValue(-15, 15), 0, 255), + (unsigned char)Clamp(84 + GetRandomValue(-15, 15), 0, 255), + 255}; + Color hillColor = (Color){ + (unsigned char)Clamp(52 + GetRandomValue(-12, 12), 0, 255), + (unsigned char)Clamp(120 + GetRandomValue(-12, 15), 0, 255), + (unsigned char)Clamp(64 + GetRandomValue(-12, 12), 0, 255), + 255}; + + Texture2D texture = GenerateIslandTexture(shoreRadiusPx, shoreColor, grassColor, hillColor); + + island->sprite = (Sprite){ .texture = texture, - .frameSize = {frameWidth, frameWidth}, - .sourceRec = {0.0f, 0.0f, frameWidth, frameWidth}, - .destRec = {GetScreenWidth() / 2, 0.0f, frameWidth, frameWidth}, - .origin = {frameWidth / 2, frameWidth / 2}, - .rotation = rotation, + .frameSize = {ISLAND_IMAGE_SIZE, ISLAND_IMAGE_SIZE}, + .sourceRec = {0.0f, 0.0f, ISLAND_IMAGE_SIZE, ISLAND_IMAGE_SIZE}, + .destRec = {position.x, position.y, worldDiameter, worldDiameter}, + .origin = {worldDiameter / 2.0f, worldDiameter / 2.0f}, + .rotation = 0.0f, .color = WHITE}; - - Physics islandPhysics = {.position = (Vector2){-100.0f, 0.0f}, - .direction = (Vector2){0.0f, 0.0f}, - .speed = 10.0f}; - - return (Islands){.sprite = islandSprite, .physics = islandPhysics}; } -void UpdateIslands(Islands *islands) { +Islands CreateIslands(Camera2D *camera) { + Islands islands = {.count = MAX_ISLANDS}; + Rectangle cameraRec = GetCameraRectangle(camera); - UpdatePhysics(&islands->physics, (Vector2){0.0f, 0.0f}); - UpdateSpriteDestRec(&islands->sprite, &islands->physics.position); - UpdateAnimation(&islands->animation, GetFrameTime()); - UpdateSpriteSourceRec( - &islands->sprite, - &(Vector2){islands->sprite.frameSize.x * islands->animation.currentFrame, - islands->sprite.frameSize.y}); + for (int i = 0; i < islands.count; i++) { + GenerateIsland(&islands.island[i], + GetPositionOutsideRectangle(cameraRec, ISLAND_MIN_SPAWN_DISTANCE, ISLAND_MAX_SPAWN_DISTANCE)); + } + + return islands; } -void RenderIslands(Islands *islands) { RenderSprite(&islands->sprite); } +void UpdateIslands(Islands *islands, Camera2D *camera) { + Rectangle cameraRec = GetCameraRectangle(camera); + + for (int i = 0; i < islands->count; i++) { + if (GetDistanceBetweenTwoVectors(islands->island[i].position, camera->target) > ISLAND_RECYCLE_DISTANCE) { + GenerateIsland(&islands->island[i], + GetPositionOutsideRectangle(cameraRec, ISLAND_MIN_SPAWN_DISTANCE, ISLAND_MAX_SPAWN_DISTANCE)); + } + } +} + +void RenderIslands(Islands *islands) { + for (int i = 0; i < islands->count; i++) { + RenderSprite(&islands->island[i].sprite); + } +} + +void UnloadIslands(Islands *islands) { + for (int i = 0; i < islands->count; i++) { + UnloadTexture(islands->island[i].sprite.texture); + } +} diff --git a/src/appGameOver.c b/src/appGameOver.c new file mode 100644 index 0000000..3f475c2 --- /dev/null +++ b/src/appGameOver.c @@ -0,0 +1,60 @@ +#include + +#include "app.h" +#include "gameover.h" + +void InitGameOver(GameOverState *g) { + int windowWidth = GetScreenWidth(); + int windowHeight = GetScreenHeight(); + Vector2 buttonDimensions = {200, 60}; + Vector2 buttonPos = {windowWidth / 2.0f, windowHeight / 2.0f + 80.0f}; + g->restartButton = initButton("Restart", buttonDimensions, buttonPos); +} + +void UpdateGameOver(App *app) { + if (isButtonClicked(&app->gameOver.restartButton)) { + ResetGame(app); + app->state.currentView = GAMEPLAY; + } + + UpdateMouseScreen(&app->game.mouse); + + OnWindowClosedStoppApp(app); +} + +void RenderGameOver(App *app) { + BeginDrawing(); + ClearBackground(BLACK); + + int windowWidth = GetScreenWidth(); + int windowHeight = GetScreenHeight(); + + const char *title = "GAME OVER"; + int titleSize = app->font.baseSize * 2; + Vector2 titleDim = MeasureTextEx(app->font, title, titleSize, 2); + DrawTextEx( + app->font, title, + (Vector2){windowWidth / 2.0f - titleDim.x / 2.0f, windowHeight / 2.0f - titleDim.y - 40.0f}, + titleSize, 2, TEXT_COLOR_LIGHT); + + const char *goldText = TextFormat("Gold collected: %i", app->game.player.gold); + int goldSize = app->font.baseSize; + Vector2 goldDim = MeasureTextEx(app->font, goldText, goldSize, 2); + DrawTextEx( + app->font, goldText, + (Vector2){windowWidth / 2.0f - goldDim.x / 2.0f, windowHeight / 2.0f - goldDim.y / 2.0f}, + goldSize, 2, TEXT_COLOR_LIGHT); + + renderButton(&app->gameOver.restartButton); + + Mouse *mouse = &app->game.mouse; + Rectangle mouseRectangle = + (Rectangle){0, 0, mouse->cursorTexture.width, mouse->cursorTexture.height}; + DrawTextureRec(mouse->cursorTexture, mouseRectangle, mouse->screenPosition, WHITE); + + if (app->flags.showFPS) { + DrawFPS(10, 10); + } + + EndDrawing(); +} diff --git a/src/appGameOverlay.c b/src/appGameOverlay.c index b5b87f1..5bd95d8 100644 --- a/src/appGameOverlay.c +++ b/src/appGameOverlay.c @@ -1,19 +1,28 @@ -#include "app.h" -#include "raylib.h" #include +#include -void RenderOverlay(Mouse *mouse) { +#include "engineInput.h" +#include "overlay.h" +#include "player.h" +#include "rose.h" - // Health - Player *player = GetPlayer(); - const char *text = TextFormat("Health: %i\n" - "Gold: %i\n", - player->health, player->gold); +Overlay CreateOverlay(void) { + return (Overlay){.rose = CreateRose()}; +} + +void UpdateOverlay(Overlay *overlay, Player *player) { + UpdateRose(&overlay->rose, player); +} + +void RenderOverlay(Mouse *mouse, Player *player, Rose *rose) { + const char *text = TextFormat( + "Health: %i\n" + "Gold: %i\n", + player->health, player->gold); DrawText(text, 0, 0, 25, WHITE); - // Mouse - Rectangle mouseRectangle = (Rectangle){0, 0, mouse->cursorTexture.width, - mouse->cursorTexture.height}; - DrawTextureRec(mouse->cursorTexture, mouseRectangle, mouse->screenPosition, - WHITE); + Rectangle mouseRectangle = + (Rectangle){0, 0, mouse->cursorTexture.width, mouse->cursorTexture.height}; + DrawTextureRec(mouse->cursorTexture, mouseRectangle, mouse->screenPosition, WHITE); + RenderRose(rose); } diff --git a/src/appGamePathfinding.c b/src/appGamePathfinding.c new file mode 100644 index 0000000..6e9e996 --- /dev/null +++ b/src/appGamePathfinding.c @@ -0,0 +1,273 @@ +#include "pathfinding.h" +#include +#include +#include +#include + +#define PATH_GRID_MAX_DIM 24 +#define PATH_MIN_CELL_SIZE 16.0f +#define PATH_GRID_MAX_NODES ((PATH_GRID_MAX_DIM + 2) * (PATH_GRID_MAX_DIM + 2)) +#define PATH_LINE_SAMPLE_STEP 8.0f +#define CLEARANCE_GRID_STEP 8.0f +#define PATH_GRID_PADDING 256.0f + +typedef struct { + float minX, minY; + float cellSize; + int cols, rows, nodeCount; + int startIdx, goalIdx; +} PathGrid; + +static bool IsKnownWater(Terrain *terrain, Vector2 pos) { + return IsTerrainLoaded(terrain, pos) && !IsTerrainLand(terrain, pos); +} + +bool IsWaterWithClearance(Terrain *terrain, Vector2 pos, float clearance) { + if (!IsKnownWater(terrain, pos)) + return false; + + int radiusSteps = (int)(clearance / CLEARANCE_GRID_STEP) + 1; + float clearanceSquared = clearance * clearance; + for (int gy = -radiusSteps; gy <= radiusSteps; gy++) { + for (int gx = -radiusSteps; gx <= radiusSteps; gx++) { + if (gx == 0 && gy == 0) + continue; + float ox = (float)gx * CLEARANCE_GRID_STEP; + float oy = (float)gy * CLEARANCE_GRID_STEP; + if (ox * ox + oy * oy > clearanceSquared) + continue; + if (!IsKnownWater(terrain, (Vector2){pos.x + ox, pos.y + oy})) + return false; + } + } + + return true; +} + +Vector2 GetRandomWaterPositionNear(Terrain *terrain, Vector2 origin, float maxRadius, + float clearance) { + float startAngle = (float)GetRandomValue(0, 359); + for (float r = 16.0f; r < maxRadius; r += 16.0f) { + for (float aStep = 0.0f; aStep < 360.0f; aStep += 30.0f) { + float a = startAngle + aStep; + Vector2 p = {origin.x + cosf(a * DEG2RAD) * r, origin.y + sinf(a * DEG2RAD) * r}; + if (IsWaterWithClearance(terrain, p, clearance)) + return p; + } + } + return origin; +} + +static bool IsLineClear(Terrain *terrain, Vector2 a, Vector2 b, float clearance) { + float dist = Vector2Distance(a, b); + int steps = (int)(dist / PATH_LINE_SAMPLE_STEP) + 1; + for (int i = 0; i <= steps; i++) { + Vector2 p = Vector2Lerp(a, b, (float)i / (float)steps); + if (!IsWaterWithClearance(terrain, p, clearance)) + return false; + } + return true; +} + +static float Heuristic(int c1, int r1, int c2, int r2, float cellSize) { + int dx = abs(c1 - c2); + int dy = abs(r1 - r2); + int mn = dx < dy ? dx : dy; + int mx = dx < dy ? dy : dx; + return cellSize * (1.41421356f * (float)mn + (float)(mx - mn)); +} + +static int ClampToGrid(int value, int size) { + if (value < 0) + return 0; + if (value >= size) + return size - 1; + return value; +} + +static PathGrid BuildPathGrid(Vector2 start, Vector2 goal) { + PathGrid grid = {0}; + + float minX = fminf(start.x, goal.x) - PATH_GRID_PADDING; + float maxX = fmaxf(start.x, goal.x) + PATH_GRID_PADDING; + float minY = fminf(start.y, goal.y) - PATH_GRID_PADDING; + float maxY = fmaxf(start.y, goal.y) + PATH_GRID_PADDING; + + float spanX = maxX - minX; + float spanY = maxY - minY; + float cellSize = fmaxf(PATH_MIN_CELL_SIZE, fmaxf(spanX, spanY) / (float)PATH_GRID_MAX_DIM); + + grid.minX = minX; + grid.minY = minY; + grid.cellSize = cellSize; + grid.cols = (int)(spanX / cellSize) + 1; + grid.rows = (int)(spanY / cellSize) + 1; + grid.nodeCount = grid.cols * grid.rows; + if (grid.nodeCount > PATH_GRID_MAX_NODES || grid.nodeCount <= 0) { + grid.nodeCount = 0; + return grid; + } + + int startCol = ClampToGrid((int)((start.x - minX) / cellSize), grid.cols); + int startRow = ClampToGrid((int)((start.y - minY) / cellSize), grid.rows); + int goalCol = ClampToGrid((int)((goal.x - minX) / cellSize), grid.cols); + int goalRow = ClampToGrid((int)((goal.y - minY) / cellSize), grid.rows); + + grid.startIdx = startRow * grid.cols + startCol; + grid.goalIdx = goalRow * grid.cols + goalCol; + return grid; +} + +static bool RunAStar(Terrain *terrain, PathGrid grid, float clearance, int *cameFrom) { + static float gScore[PATH_GRID_MAX_NODES]; + static float fScore[PATH_GRID_MAX_NODES]; + static bool inOpen[PATH_GRID_MAX_NODES]; + static bool inClosed[PATH_GRID_MAX_NODES]; + + int cols = grid.cols, rows = grid.rows; + int goalCol = grid.goalIdx % cols, goalRow = grid.goalIdx / cols; + + for (int i = 0; i < grid.nodeCount; i++) { + gScore[i] = INFINITY; + cameFrom[i] = -1; + inOpen[i] = false; + inClosed[i] = false; + } + + gScore[grid.startIdx] = 0.0f; + fScore[grid.startIdx] = + Heuristic(grid.startIdx % cols, grid.startIdx / cols, goalCol, goalRow, grid.cellSize); + inOpen[grid.startIdx] = true; + int openCount = 1; + + static const int neighborCol[8] = {-1, 1, 0, 0, -1, -1, 1, 1}; + static const int neighborRow[8] = {0, 0, -1, 1, -1, 1, -1, 1}; + + while (openCount > 0) { + int current = -1; + float best = INFINITY; + for (int i = 0; i < grid.nodeCount; i++) { + if (inOpen[i] && fScore[i] < best) { + best = fScore[i]; + current = i; + } + } + if (current == -1) + break; + if (current == grid.goalIdx) + return true; + + inOpen[current] = false; + openCount--; + inClosed[current] = true; + + int curCol = current % cols; + int curRow = current / cols; + + for (int n = 0; n < 8; n++) { + int nc = curCol + neighborCol[n]; + int nr = curRow + neighborRow[n]; + if (nc < 0 || nc >= cols || nr < 0 || nr >= rows) + continue; + + int nIdx = nr * cols + nc; + if (inClosed[nIdx]) + continue; + + Vector2 cellCenter = {grid.minX + ((float)nc + 0.5f) * grid.cellSize, + grid.minY + ((float)nr + 0.5f) * grid.cellSize}; + if (!IsWaterWithClearance(terrain, cellCenter, clearance)) + continue; + + if (neighborCol[n] != 0 && neighborRow[n] != 0) { + Vector2 side1 = {grid.minX + ((float)(curCol + neighborCol[n]) + 0.5f) * grid.cellSize, + grid.minY + ((float)curRow + 0.5f) * grid.cellSize}; + Vector2 side2 = {grid.minX + ((float)curCol + 0.5f) * grid.cellSize, + grid.minY + ((float)(curRow + neighborRow[n]) + 0.5f) * grid.cellSize}; + if (!IsWaterWithClearance(terrain, side1, clearance) || + !IsWaterWithClearance(terrain, side2, clearance)) + continue; + } + + float moveCost = (neighborCol[n] != 0 && neighborRow[n] != 0) ? grid.cellSize * 1.41421356f + : grid.cellSize; + float tentativeG = gScore[current] + moveCost; + if (tentativeG < gScore[nIdx]) { + cameFrom[nIdx] = current; + gScore[nIdx] = tentativeG; + fScore[nIdx] = tentativeG + Heuristic(nc, nr, goalCol, goalRow, grid.cellSize); + if (!inOpen[nIdx]) { + inOpen[nIdx] = true; + openCount++; + } + } + } + } + + return false; +} + +static int ReconstructRawPath(PathGrid grid, const int *cameFrom, Vector2 goal, + Vector2 *rawPoints) { + int rawCount = 0; + for (int cur = grid.goalIdx; cur != -1; cur = cameFrom[cur]) { + int c = cur % grid.cols; + int r = cur / grid.cols; + rawPoints[rawCount++] = (Vector2){grid.minX + ((float)c + 0.5f) * grid.cellSize, + grid.minY + ((float)r + 0.5f) * grid.cellSize}; + } + + for (int i = 0; i < rawCount / 2; i++) { + Vector2 tmp = rawPoints[i]; + rawPoints[i] = rawPoints[rawCount - 1 - i]; + rawPoints[rawCount - 1 - i] = tmp; + } + rawPoints[rawCount - 1] = goal; + + return rawCount; +} + +static int SimplifyPathWithStringPulling(Terrain *terrain, Vector2 start, const Vector2 *rawPoints, + int rawCount, float clearance, Vector2 *outWaypoints) { + int count = 0; + int i = 0; + Vector2 anchor = start; + while (i < rawCount - 1 && count < PATH_MAX_WAYPOINTS) { + int farthest = i + 1; + for (int j = rawCount - 1; j > i; j--) { + if (IsLineClear(terrain, anchor, rawPoints[j], clearance)) { + farthest = j; + break; + } + } + anchor = rawPoints[farthest]; + outWaypoints[count++] = anchor; + i = farthest; + } + return count; +} + +Path FindWaterPath(Terrain *terrain, Vector2 start, Vector2 goal, float clearance) { + Path path = {0}; + + if (IsLineClear(terrain, start, goal, clearance)) { + path.waypoints[0] = goal; + path.count = 1; + return path; + } + + PathGrid grid = BuildPathGrid(start, goal); + if (grid.nodeCount == 0) + return path; + + static int cameFrom[PATH_GRID_MAX_NODES]; + if (!RunAStar(terrain, grid, clearance, cameFrom)) + return path; + + static Vector2 rawPoints[PATH_GRID_MAX_NODES]; + int rawCount = ReconstructRawPath(grid, cameFrom, goal, rawPoints); + + path.count = + SimplifyPathWithStringPulling(terrain, start, rawPoints, rawCount, clearance, path.waypoints); + return path; +} diff --git a/src/appGamePlayer.c b/src/appGamePlayer.c index f0878f4..12e7363 100644 --- a/src/appGamePlayer.c +++ b/src/appGamePlayer.c @@ -1,8 +1,27 @@ -#include "app.h" -#include "engine.h" +#include + +#include "appCore.h" +#include "engineAssets.h" +#include "engineConfig.h" +#include "enginePhysics.h" +#include "engineSprite.h" +#include "engineStorage.h" +#include "engineVector.h" +#include "pathfinding.h" +#include "player.h" typedef enum { FLAPPING, SOUND_COUNT } SoundID; +#define PLAYER_SAVE_VERSION 1 + +typedef struct { + int version; + int gold; + float posX; + float posY; + float rotation; +} PlayerSaveData; + const SoundAsset playerSoundAssets[SOUND_COUNT] = {{"assets/flapping.wav"}}; const TextureAsset playerTextureAssets[PLAYER_TEX_COUNT] = { @@ -10,78 +29,94 @@ const TextureAsset playerTextureAssets[PLAYER_TEX_COUNT] = { float playerSpeeds[SPEED_COUNT] = {0.0f, 5.0f, 10.0f}; -Config config; +#define PLAYER_TERRAIN_CLEARANCE 40.0f +#define PLAYER_SPAWN_MAX_RADIUS 400.0f -playerSpeedID GetPlayerSpeedID(float speed) { +PlayerSpeedID GetPlayerSpeedID(float speed) { for (int i = 0; i < SPEED_COUNT; i++) { if (playerSpeeds[i] == speed) { - return (playerSpeedID)i; + return (PlayerSpeedID)i; } } return ANCHOR; } -Player *GetPlayer(void) { - static Player player; - return &player; +static void RandomizeSpawn(Player *player, Terrain *terrain) { + player->physics.position = GetRandomWaterPositionNear( + terrain, (Vector2){0.0f, 0.0f}, PLAYER_SPAWN_MAX_RADIUS, PLAYER_TERRAIN_CLEARANCE); + player->physics.direction = (Vector2){0.0f, 0.0f}; + player->physics.speed = playerSpeeds[ANCHOR]; + player->rotation = (float)GetRandomValue(0, 359); + player->sprite.rotation = player->rotation; } -void CreatePlayer(void) { - InitConfig(&config, "config/player.ini"); +Player CreatePlayer(Terrain *terrain) { + Player player = {0}; + InitConfig(&player.config, "config/player.ini"); - int PLAYER_HEALTH_MAX = GetConfigInt(&config, "HEALTH_MAX"); - int START_GOLD = GetConfigInt(&config, "START_GOLD"); + int PLAYER_HEALTH_MAX = GetConfigInt(&player.config, "HEALTH_MAX"); + int START_GOLD = GetConfigInt(&player.config, "START_GOLD"); TextureAsset startShipAsset = playerTextureAssets[START_SHIP]; - - Texture2D texture = LoadTexture(startShipAsset.path); + Texture2D texture = LoadTextureCached(startShipAsset.path); float frameWidth = startShipAsset.frameWidth; - float rotation = - (float)LoadStorageValue(STORAGE_DATA_FILE, STORAGE_POSITION_ROTATION); - Sprite playerSprite = {.texture = texture, - .frameSize = {frameWidth, frameWidth}, - .sourceRec = {0.0f, 0.0f, frameWidth, frameWidth}, - .destRec = {0.0f, 0.0f, frameWidth, frameWidth}, - .origin = {frameWidth / 2, frameWidth / 2}, - .rotation = rotation, - .color = WHITE}; + PlayerSaveData saveData = {0}; + LoadStorageData(STORAGE_DATA_FILE, PLAYER_SAVE_VERSION, &saveData, sizeof(saveData)); - Animation playerAnimation = {.state = 1, - .currentFrame = 0, - .maxFrame = 3, - .framesCounter = 0, - .framesSpeed = 0.0f, - .animTimer = 0.0f}; + player.sprite = (Sprite){.texture = texture, + .frameSize = {frameWidth, frameWidth}, + .sourceRec = {0.0f, 0.0f, frameWidth, frameWidth}, + .destRec = {0.0f, 0.0f, frameWidth, frameWidth}, + .origin = {frameWidth / 2, frameWidth / 2}, + .rotation = 0.0f, + .color = WHITE}; - Physics playerPhysics = { - .position = {(float)LoadStorageValue(STORAGE_DATA_FILE, - STORAGE_POSITION_POSX), - (float)LoadStorageValue(STORAGE_DATA_FILE, - STORAGE_POSITION_POSY)}, - .direction = {0.0f, 0.0f}, - .speed = playerSpeeds[ANCHOR], - .weight = 10.0f}; + player.animation = (Animation){.state = 1, + .currentFrame = 0, + .maxFrame = 3, + .framesCounter = 0, + .framesSpeed = 0.0f, + .animTimer = 0.0f}; - Player *player = GetPlayer(); + player.physics = (Physics){.position = {0.0f, 0.0f}, + .direction = {0.0f, 0.0f}, + .speed = playerSpeeds[ANCHOR], + .weight = 10.0f}; - *player = (Player){ - .sprite = playerSprite, - .animation = playerAnimation, - .physics = playerPhysics, - .rotation = rotation, - .soundFlapping = LoadSound(playerSoundAssets[FLAPPING].path), - .health = PLAYER_HEALTH_MAX, - .gold = LoadStorageValue(STORAGE_DATA_FILE, STORAGE_POSITION_GOLD)}; + player.rotation = 0.0f; + player.soundFlapping = LoadSoundCached(playerSoundAssets[FLAPPING].path); + player.health = PLAYER_HEALTH_MAX; + player.gold = saveData.gold; - if (player->gold == 0) { - player->gold = START_GOLD; + if (player.gold == 0) { + player.gold = START_GOLD; } + + RandomizeSpawn(&player, terrain); + + return player; } -void UpdatePlayer(void) { - Player *player = GetPlayer(); - if (IsKeyPressed(KEY_W) || IsKeyPressed(KEY_UP)) { +void ResetPlayer(Player *player, Terrain *terrain) { + int PLAYER_HEALTH_MAX = GetConfigInt(&player->config, "HEALTH_MAX"); + player->health = PLAYER_HEALTH_MAX; + RandomizeSpawn(player, terrain); + ResetWakeTrail(&player->wakeTrail); + SavePlayer(player); +} + +void SavePlayer(Player *player) { + PlayerSaveData saveData = {.version = PLAYER_SAVE_VERSION, + .gold = player->gold, + .posX = player->physics.position.x, + .posY = player->physics.position.y, + .rotation = player->rotation}; + SaveStorageData(STORAGE_DATA_FILE, PLAYER_SAVE_VERSION, &saveData, sizeof(saveData)); +} + +static void UpdatePlayerSpeedFromInput(Player *player) { + if (IsGameActionPressed(ACTION_FORWARD)) { if (player->physics.speed < playerSpeeds[FAST_AHEAD]) { if (player->physics.speed < playerSpeeds[SLOW_AHEAD]) { player->physics.speed = playerSpeeds[SLOW_AHEAD]; @@ -91,7 +126,7 @@ void UpdatePlayer(void) { } } - if (IsKeyPressed(KEY_S) || IsKeyPressed(KEY_DOWN)) { + if (IsGameActionPressed(ACTION_BACKWARD)) { if (player->physics.speed > playerSpeeds[ANCHOR]) { if (player->physics.speed > playerSpeeds[SLOW_AHEAD]) { player->physics.speed = playerSpeeds[SLOW_AHEAD]; @@ -100,35 +135,36 @@ void UpdatePlayer(void) { } } } +} +static void UpdatePlayerRotationFromInput(Player *player) { + float rotationSpeed = 50.0f; + if (player->physics.speed <= playerSpeeds[ANCHOR]) + return; + + if (IsGameActionDown(ACTION_TURN_LEFT)) { + player->rotation -= rotationSpeed * GetFrameTime(); + } + + if (IsGameActionDown(ACTION_TURN_RIGHT)) { + player->rotation += rotationSpeed * GetFrameTime(); + } +} + +void UpdatePlayer(Player *player) { + UpdatePlayerSpeedFromInput(player); player->sprite.sourceRec.x = 16.0f * GetPlayerSpeedID(player->physics.speed); - float rotationSpeed = 50.0f; - - if (player->physics.speed > playerSpeeds[ANCHOR]) { - - if (IsKeyDown(KEY_A) || IsKeyDown(KEY_LEFT)) { - player->rotation -= rotationSpeed * GetFrameTime(); - } - - if (IsKeyDown(KEY_D) || IsKeyDown(KEY_RIGHT)) { - player->rotation += rotationSpeed * GetFrameTime(); - } - } + UpdatePlayerRotationFromInput(player); player->sprite.rotation = player->rotation; Vector2 dir = RotationToVector2(player->rotation - 90.0f); UpdatePhysics(&player->physics, dir); UpdateSpriteDestRec(&player->sprite, &player->physics.position); - SaveStorageValue(STORAGE_DATA_FILE, STORAGE_POSITION_POSX, - (int)player->physics.position.x); - SaveStorageValue(STORAGE_DATA_FILE, STORAGE_POSITION_POSY, - (int)player->physics.position.y); - SaveStorageValue(STORAGE_DATA_FILE, STORAGE_POSITION_ROTATION, - (int)player->rotation); + UpdateWakeTrail(&player->wakeTrail, player->physics.position); + SavePlayer(player); } -void RenderPlayer(void) { - Player *player = GetPlayer(); +void RenderPlayer(Player *player) { RenderSprite(&player->sprite); } diff --git a/src/appGameProjectiles.c b/src/appGameProjectiles.c index e71275c..c216098 100644 --- a/src/appGameProjectiles.c +++ b/src/appGameProjectiles.c @@ -1,94 +1,99 @@ -#include "app.h" #include #include +#include "appCore.h" +#include "engineAssets.h" +#include "enginePhysics.h" +#include "projectiles.h" + typedef enum { CANNONBALL_SHOOTING, CANNONBALL_IMPACT, SOUND_COUNT } SoundID; const SoundAsset soundAssets[SOUND_COUNT] = {{"assets/cannonball_shooting.mp3"}, {"assets/cannonball_impact.mp3"}}; +#define PROJECTILE_RELOAD_TIME 1.0f +#define PROJECTILE_MUZZLE_OFFSET 12.0f + Projectiles CreateProjectiles(void) { Projectiles projectiles = {.count = MAX_PROJECTILES}; + Sound shootingSound = LoadSoundCached(soundAssets[CANNONBALL_SHOOTING].path); + Sound impactSound = LoadSoundCached(soundAssets[CANNONBALL_IMPACT].path); + for (int i = 0; i < projectiles.count; i++) { - projectiles.physics[i] = - (Physics){(Vector2){0.0f, 0.0f}, (Vector2){0.0f, 0.0f}, - (Vector2){0.0f, 0.0f}, 50.0f}; + projectiles.physics[i] = (Physics){(Vector2){0.0f, 0.0f}, (Vector2){0.0f, 0.0f}, + (Vector2){0.0f, 0.0f}, PROJECTILE_SPEED}; projectiles.radius[i] = 1.0f; projectiles.active[i] = false; + projectiles.friendly[i] = true; projectiles.color[i] = GRAY; - projectiles.soundShooting[i] = - LoadSound(soundAssets[CANNONBALL_SHOOTING].path); - projectiles.soundImpact[i] = LoadSound(soundAssets[CANNONBALL_IMPACT].path); + projectiles.soundShooting[i] = LoadSoundAlias(shootingSound); + projectiles.soundImpact[i] = LoadSoundAlias(impactSound); } return projectiles; } -void UpdateProjectiles(Projectiles *projectiles, Enemies *enemies) { +bool FireProjectile(Projectiles *projectiles, Vector2 position, Vector2 direction, bool friendly) { + for (int i = 0; i < projectiles->count; i++) { + if (projectiles->active[i]) + continue; + projectiles->active[i] = true; + projectiles->friendly[i] = friendly; + projectiles->physics[i].position = position; + projectiles->physics[i].direction = direction; + projectiles->color[i] = friendly ? GRAY : RED; + SetSoundPitch(projectiles->soundShooting[i], (float)GetRandomValue(90, 110) / 100.0f); + SetSoundVolume(projectiles->soundShooting[i], (float)GetRandomValue(85, 100) / 100.0f); + PlaySound(projectiles->soundShooting[i]); + return true; + } + return false; +} + +static bool TryFirePlayerBroadside(Projectiles *projectiles, Player *player, GameAction action, + float sideDegrees, float currentTime, float *lastShotTime) { + if (!IsGameActionDown(action) || (currentTime - *lastShotTime < PROJECTILE_RELOAD_TIME)) { + return false; + } + + Vector2 direction = Vector2Rotate(player->physics.direction, sideDegrees * DEG2RAD); + Vector2 firePosition = + Vector2Add(player->physics.position, Vector2Scale(direction, PROJECTILE_MUZZLE_OFFSET)); + if (FireProjectile(projectiles, firePosition, direction, true)) { + *lastShotTime = currentTime; + return true; + } + return false; +} + +void UpdateProjectiles(Projectiles *projectiles, Player *player) { static float lastShotTime = 0.0f; - const float reloadTime = 1.0f; float currentTime = GetTime(); - if (IsKeyDown(KEY_Q) && (currentTime - lastShotTime >= reloadTime)) { - for (int i = 0; i < projectiles->count; i++) { - if (!projectiles->active[i]) { - projectiles->active[i] = true; - Player *player = GetPlayer(); - projectiles->physics[i].position = player->physics.position; - projectiles->physics[i].direction = - Vector2Rotate(player->physics.direction, -90.0f * DEG2RAD); - PlaySound(projectiles->soundShooting[i]); - lastShotTime = currentTime; - break; - } - } - } - - if (IsKeyDown(KEY_E) && (currentTime - lastShotTime >= reloadTime)) { - for (int i = 0; i < projectiles->count; i++) { - if (!projectiles->active[i]) { - projectiles->active[i] = true; - Player *player = GetPlayer(); - projectiles->physics[i].position = player->physics.position; - projectiles->physics[i].direction = - Vector2Rotate(player->physics.direction, 90.0f * DEG2RAD); - PlaySound(projectiles->soundShooting[i]); - lastShotTime = currentTime; - break; - } - } - } + TryFirePlayerBroadside(projectiles, player, ACTION_FIRE_PORT, -90.0f, currentTime, &lastShotTime); + TryFirePlayerBroadside(projectiles, player, ACTION_FIRE_STARBOARD, 90.0f, currentTime, + &lastShotTime); for (int i = 0; i < projectiles->count; i++) { - if (!projectiles->active[i]) continue; - for (int j = 0; j < enemies->count; j++) { - if (CheckCollisionCircleRec(projectiles->physics[i].position, - projectiles->radius[i], - enemies->sprite[j].destRec)) { - PlaySound(projectiles->soundImpact[i]); - Player *player = GetPlayer(); - player->gold += 1; - SaveStorageValue(STORAGE_DATA_FILE, STORAGE_POSITION_GOLD, - player->gold); - projectiles->active[i] = false; - } - } - UpdatePhysics(&projectiles->physics[i], projectiles->physics[i].direction); } } +void ResetProjectiles(Projectiles *projectiles) { + for (int i = 0; i < projectiles->count; i++) { + projectiles->active[i] = false; + } +} + void RenderProjectiles(Projectiles *projectiles) { for (int i = 0; i < projectiles->count; i++) { if (projectiles->active[i] == true) { - DrawCircle(projectiles->physics[i].position.x, - projectiles->physics[i].position.y, projectiles->radius[i], - projectiles->color[i]); + DrawCircleV(projectiles->physics[i].position, projectiles->radius[i], projectiles->color[i]); } } } diff --git a/src/appGameRose.c b/src/appGameRose.c new file mode 100644 index 0000000..812dae0 --- /dev/null +++ b/src/appGameRose.c @@ -0,0 +1,59 @@ +#include +#include "appCore.h" +#include "engineAssets.h" +#include "rose.h" +#include "engineSprite.h" + +const TextureAsset roseTextureAssets[ROSE_TEX_COUNT] = {{ + "assets/rose.png", + 2560.0f, + 2560.0f, + 0.0f +}}; + +Rose CreateRose(void){ + + TextureAsset roseAsset = roseTextureAssets[ROSE]; + Texture2D texture = LoadTextureCached(roseAsset.path); + float frameWidth = roseAsset.frameWidth; + Vector2 frameSize = {frameWidth, frameWidth}; + float rotation = roseAsset.rotation; + Rectangle sourceRec = {0.0f, 0.0f, frameWidth, frameWidth}; + float destSize = GetScreenWidth() * ROSE_SCREEN_WIDTH_FRACTION; + Rectangle destRec = {GetScreenWidth() / 2, GetScreenHeight(), destSize, destSize}; + Vector2 origin = {destSize / 2, destSize / 2}; + Color color = WHITE; + + Sprite sprite = { + .texture = texture, + .frameSize = frameSize, + .sourceRec = sourceRec, + .destRec = destRec, + .origin = origin, + .rotation = rotation, + .color = color + }; + + Rose rose = { + .sprite = sprite + }; + + return rose; + +} + +void UpdateRose(Rose *rose, Player *player){ + float rotationSpeed = player->rotation; + + if (IsGameActionDown(ACTION_TURN_LEFT)) { + rose->sprite.rotation -= rotationSpeed * GetFrameTime(); + } + + if (IsGameActionDown(ACTION_TURN_RIGHT)) { + rose->sprite.rotation += rotationSpeed * GetFrameTime(); + } +} + +void RenderRose(Rose *rose){ + RenderSprite(&rose->sprite); +} diff --git a/src/appGameTerrain.c b/src/appGameTerrain.c new file mode 100644 index 0000000..8d480a0 --- /dev/null +++ b/src/appGameTerrain.c @@ -0,0 +1,286 @@ +#include +#include +#include + +#include "terrain.h" + +#define TERRAIN_BASE_SCALE 2.2f +#define TERRAIN_DETAIL_SCALE 9.0f +#define TERRAIN_DETAIL_WEIGHT 0.22f + +#define TERRAIN_WATER_LEVEL 0.62f +#define TERRAIN_SAND_BAND 0.035f +#define TERRAIN_GRASS_BAND 0.12f + +#define TERRAIN_SHORE_BAND_RADIUS 3 + +#define TERRAIN_STREAM_BUDGET 2 + +static int WorldToChunkCoord(float worldCoord) { + return (int)floorf(worldCoord / TERRAIN_CHUNK_WORLD_SIZE); +} + +static void GenerateChunkHeight(int chunkX, int chunkY, int padding, int paddedResolution, + float *outHeight) { + int offsetX = chunkX * TERRAIN_CHUNK_RESOLUTION - padding; + int offsetY = chunkY * TERRAIN_CHUNK_RESOLUTION - padding; + + Image base = + GenImagePerlinNoise(paddedResolution, paddedResolution, offsetX, offsetY, TERRAIN_BASE_SCALE); + Image detail = GenImagePerlinNoise(paddedResolution, paddedResolution, offsetX, offsetY, + TERRAIN_DETAIL_SCALE); + + for (int y = 0; y < paddedResolution; y++) { + for (int x = 0; x < paddedResolution; x++) { + float baseValue = GetImageColor(base, x, y).r / 255.0f; + float detailValue = GetImageColor(detail, x, y).r / 255.0f; + outHeight[y * paddedResolution + x] = + baseValue * (1.0f - TERRAIN_DETAIL_WEIGHT) + detailValue * TERRAIN_DETAIL_WEIGHT; + } + } + + UnloadImage(base); + UnloadImage(detail); +} + +static Color GetLandColor(float height) { + Color sandColor = {233, 220, 178, 255}; + Color grassColor = {80, 168, 84, 255}; + Color hillColor = {52, 120, 64, 255}; + + if (height < TERRAIN_WATER_LEVEL + TERRAIN_SAND_BAND) + return sandColor; + if (height < TERRAIN_WATER_LEVEL + TERRAIN_GRASS_BAND) + return grassColor; + return hillColor; +} + +static Color GetShoreAlphaColor(const float *height, int paddedResolution, int px, int py) { + float nearestDistSq = 1e9f; + int r = TERRAIN_SHORE_BAND_RADIUS; + for (int dy = -r; dy <= r; dy++) { + for (int dx = -r; dx <= r; dx++) { + if (height[(py + dy) * paddedResolution + (px + dx)] >= TERRAIN_WATER_LEVEL) { + float distSq = (float)(dx * dx + dy * dy); + if (distSq < nearestDistSq) + nearestDistSq = distSq; + } + } + } + + if (nearestDistSq > (float)(r * r)) + return BLANK; + + float t = 1.0f - sqrtf(nearestDistSq) / (float)r; + float alphaValue = t * 254.0f; + if (alphaValue < 1.0f) + alphaValue = 1.0f; + if (alphaValue > 254.0f) + alphaValue = 254.0f; + return (Color){255, 255, 255, (unsigned char)alphaValue}; +} + +static void PaintChunkPixel(Image *image, bool *landMask, const float *height, int paddedResolution, + int padding, int x, int y) { + int px = x + padding, py = y + padding; + float h = height[py * paddedResolution + px]; + bool isLand = h >= TERRAIN_WATER_LEVEL; + landMask[y * TERRAIN_CHUNK_RESOLUTION + x] = isLand; + + if (isLand) { + ImageDrawPixel(image, x, y, GetLandColor(h)); + return; + } + + Color shoreColor = GetShoreAlphaColor(height, paddedResolution, px, py); + if (shoreColor.a > 0) { + ImageDrawPixel(image, x, y, shoreColor); + } +} + +static void GenerateChunk(TerrainChunk *chunk, int chunkX, int chunkY) { + if (chunk->active) { + UnloadTexture(chunk->sprite.texture); + free(chunk->landMask); + } + chunk->landMask = malloc(TERRAIN_CHUNK_RESOLUTION * TERRAIN_CHUNK_RESOLUTION * sizeof(bool)); + + int padding = TERRAIN_SHORE_BAND_RADIUS; + int paddedResolution = TERRAIN_CHUNK_RESOLUTION + padding * 2; + float *height = malloc((size_t)paddedResolution * (size_t)paddedResolution * sizeof(float)); + GenerateChunkHeight(chunkX, chunkY, padding, paddedResolution, height); + + Image image = GenImageColor(TERRAIN_CHUNK_RESOLUTION, TERRAIN_CHUNK_RESOLUTION, BLANK); + + for (int y = 0; y < TERRAIN_CHUNK_RESOLUTION; y++) { + for (int x = 0; x < TERRAIN_CHUNK_RESOLUTION; x++) { + PaintChunkPixel(&image, chunk->landMask, height, paddedResolution, padding, x, y); + } + } + + free(height); + + Texture2D texture = LoadTextureFromImage(image); + UnloadImage(image); + SetTextureFilter(texture, TEXTURE_FILTER_POINT); + + chunk->active = true; + chunk->chunkX = chunkX; + chunk->chunkY = chunkY; + chunk->sprite = + (Sprite){.texture = texture, + .frameSize = {TERRAIN_CHUNK_RESOLUTION, TERRAIN_CHUNK_RESOLUTION}, + .sourceRec = {0.0f, 0.0f, TERRAIN_CHUNK_RESOLUTION, TERRAIN_CHUNK_RESOLUTION}, + .destRec = {chunkX * TERRAIN_CHUNK_WORLD_SIZE, chunkY * TERRAIN_CHUNK_WORLD_SIZE, + TERRAIN_CHUNK_WORLD_SIZE, TERRAIN_CHUNK_WORLD_SIZE}, + .origin = {0.0f, 0.0f}, + .rotation = 0.0f, + .color = WHITE}; +} + +static bool IsChunkLoaded(Terrain *terrain, int chunkX, int chunkY) { + for (int i = 0; i < TERRAIN_MAX_CHUNKS; i++) { + if (terrain->chunks[i].active && terrain->chunks[i].chunkX == chunkX && + terrain->chunks[i].chunkY == chunkY) { + return true; + } + } + return false; +} + +Terrain CreateTerrain(Camera2D *camera) { + Terrain terrain = {0}; + + terrain.shader = LoadShader("shaders/terrain.vs", "shaders/terrain.fs"); + terrain.time = 0.0f; + + int centerX = WorldToChunkCoord(camera->target.x); + int centerY = WorldToChunkCoord(camera->target.y); + + int slot = 0; + for (int cy = centerY - TERRAIN_LOAD_RADIUS; cy <= centerY + TERRAIN_LOAD_RADIUS; cy++) { + for (int cx = centerX - TERRAIN_LOAD_RADIUS; cx <= centerX + TERRAIN_LOAD_RADIUS; cx++) { + GenerateChunk(&terrain.chunks[slot], cx, cy); + slot++; + } + } + + return terrain; +} + +static void UnloadChunk(TerrainChunk *chunk) { + UnloadTexture(chunk->sprite.texture); + free(chunk->landMask); + chunk->landMask = NULL; + chunk->active = false; +} + +void UpdateTerrain(Terrain *terrain, Camera2D *camera) { + int centerX = WorldToChunkCoord(camera->target.x); + int centerY = WorldToChunkCoord(camera->target.y); + + for (int i = 0; i < TERRAIN_MAX_CHUNKS; i++) { + if (!terrain->chunks[i].active) + continue; + + int dx = terrain->chunks[i].chunkX - centerX; + int dy = terrain->chunks[i].chunkY - centerY; + if (dx < -TERRAIN_LOAD_RADIUS || dx > TERRAIN_LOAD_RADIUS || dy < -TERRAIN_LOAD_RADIUS || + dy > TERRAIN_LOAD_RADIUS) { + UnloadChunk(&terrain->chunks[i]); + } + } + + int budget = TERRAIN_STREAM_BUDGET; + for (int cy = centerY - TERRAIN_LOAD_RADIUS; cy <= centerY + TERRAIN_LOAD_RADIUS && budget > 0; + cy++) { + for (int cx = centerX - TERRAIN_LOAD_RADIUS; cx <= centerX + TERRAIN_LOAD_RADIUS && budget > 0; + cx++) { + if (IsChunkLoaded(terrain, cx, cy)) + continue; + + for (int i = 0; i < TERRAIN_MAX_CHUNKS; i++) { + if (!terrain->chunks[i].active) { + GenerateChunk(&terrain->chunks[i], cx, cy); + budget--; + break; + } + } + } + } + + terrain->time += GetFrameTime(); + int timeLoc = GetShaderLocation(terrain->shader, "time"); + SetShaderValue(terrain->shader, timeLoc, &terrain->time, SHADER_UNIFORM_FLOAT); +} + +void RenderTerrain(Terrain *terrain) { + BeginShaderMode(terrain->shader); + for (int i = 0; i < TERRAIN_MAX_CHUNKS; i++) { + if (terrain->chunks[i].active) { + RenderSprite(&terrain->chunks[i].sprite); + } + } + EndShaderMode(); +} + +void UnloadTerrain(Terrain *terrain) { + UnloadShader(terrain->shader); + for (int i = 0; i < TERRAIN_MAX_CHUNKS; i++) { + if (terrain->chunks[i].active) { + UnloadChunk(&terrain->chunks[i]); + } + } +} + +static TerrainChunk *GetChunkAt(Terrain *terrain, Vector2 worldPos, int *outPx, int *outPy) { + int cx = WorldToChunkCoord(worldPos.x); + int cy = WorldToChunkCoord(worldPos.y); + + for (int i = 0; i < TERRAIN_MAX_CHUNKS; i++) { + TerrainChunk *chunk = &terrain->chunks[i]; + if (!chunk->active || chunk->chunkX != cx || chunk->chunkY != cy) + continue; + + float localX = worldPos.x - (float)cx * TERRAIN_CHUNK_WORLD_SIZE; + float localY = worldPos.y - (float)cy * TERRAIN_CHUNK_WORLD_SIZE; + + int px = (int)(localX / TERRAIN_CHUNK_WORLD_SIZE * TERRAIN_CHUNK_RESOLUTION); + int py = (int)(localY / TERRAIN_CHUNK_WORLD_SIZE * TERRAIN_CHUNK_RESOLUTION); + if (px < 0) + px = 0; + if (px >= TERRAIN_CHUNK_RESOLUTION) + px = TERRAIN_CHUNK_RESOLUTION - 1; + if (py < 0) + py = 0; + if (py >= TERRAIN_CHUNK_RESOLUTION) + py = TERRAIN_CHUNK_RESOLUTION - 1; + + *outPx = px; + *outPy = py; + return chunk; + } + + return NULL; +} + +bool IsTerrainLand(Terrain *terrain, Vector2 worldPos) { + int px, py; + TerrainChunk *chunk = GetChunkAt(terrain, worldPos, &px, &py); + if (!chunk) + return false; + return chunk->landMask[py * TERRAIN_CHUNK_RESOLUTION + px]; +} + +bool IsTerrainLoaded(Terrain *terrain, Vector2 worldPos) { + int cx = WorldToChunkCoord(worldPos.x); + int cy = WorldToChunkCoord(worldPos.y); + + for (int i = 0; i < TERRAIN_MAX_CHUNKS; i++) { + TerrainChunk *chunk = &terrain->chunks[i]; + if (chunk->active && chunk->chunkX == cx && chunk->chunkY == cy) + return true; + } + + return false; +} diff --git a/src/appGameWake.c b/src/appGameWake.c new file mode 100644 index 0000000..eea967e --- /dev/null +++ b/src/appGameWake.c @@ -0,0 +1,120 @@ +#include + +#include "wake.h" + +#define WAKE_TRAIL_MIN_SAMPLE_DIST 2.5f +#define WAKE_TRAIL_MAX_AGE 1.8 +#define WAKE_MIN_SPEED 0.5f +#define WAKE_MAX_SPEED 10.0f + +static void ExpireOldWakePoints(WakeTrail *trail, double now) { + while (trail->count > 0 && now - trail->timestamps[trail->count - 1] > WAKE_TRAIL_MAX_AGE) { + trail->count--; + } +} + +static bool HasTraveledFarEnough(WakeTrail *trail, Vector2 position) { + if (trail->count == 0) + return true; + + float dx = position.x - trail->points[0].x; + float dy = position.y - trail->points[0].y; + return dx * dx + dy * dy >= WAKE_TRAIL_MIN_SAMPLE_DIST * WAKE_TRAIL_MIN_SAMPLE_DIST; +} + +void UpdateWakeTrail(WakeTrail *trail, Vector2 position) { + double now = GetTime(); + ExpireOldWakePoints(trail, now); + + if (!HasTraveledFarEnough(trail, position)) + return; + + for (int i = WAKE_TRAIL_POINTS - 1; i > 0; i--) { + trail->points[i] = trail->points[i - 1]; + trail->timestamps[i] = trail->timestamps[i - 1]; + } + trail->points[0] = position; + trail->timestamps[0] = now; + if (trail->count < WAKE_TRAIL_POINTS) + trail->count++; +} + +void ResetWakeTrail(WakeTrail *trail) { + trail->count = 0; +} + +Wake CreateWake(void) { + Wake wake = {0}; + wake.shader = LoadShader("shaders/wake.vs", "shaders/wake.fs"); + wake.time = 0.0f; + return wake; +} + +void UpdateWake(Wake *wake, float deltaTime) { + wake->time += deltaTime; + int timeLoc = GetShaderLocation(wake->shader, "time"); + SetShaderValue(wake->shader, timeLoc, &wake->time, SHADER_UNIFORM_FLOAT); +} + +static void ComputeWakeAges(WakeTrail *trail, double now, float *outAges) { + for (int i = 0; i < WAKE_TRAIL_POINTS; i++) { + if (i >= trail->count) { + outAges[i] = 1.0f; + continue; + } + float age = (float)((now - trail->timestamps[i]) / WAKE_TRAIL_MAX_AGE); + outAges[i] = age < 0.0f ? 0.0f : (age > 1.0f ? 1.0f : age); + } +} + +static Rectangle ComputeWakeBoundingBox(WakeTrail *trail) { + float minX = trail->points[0].x, maxX = trail->points[0].x; + float minY = trail->points[0].y, maxY = trail->points[0].y; + for (int i = 1; i < trail->count; i++) { + if (trail->points[i].x < minX) + minX = trail->points[i].x; + if (trail->points[i].x > maxX) + maxX = trail->points[i].x; + if (trail->points[i].y < minY) + minY = trail->points[i].y; + if (trail->points[i].y > maxY) + maxY = trail->points[i].y; + } + + float margin = 10.0f; + return (Rectangle){minX - margin, minY - margin, (maxX - minX) + margin * 2.0f, + (maxY - minY) + margin * 2.0f}; +} + +void RenderShipWake(Wake *wake, WakeTrail *trail, float speed) { + if (trail->count < 2) + return; + + float t = (fabsf(speed) - WAKE_MIN_SPEED) / (WAKE_MAX_SPEED - WAKE_MIN_SPEED); + if (t < 0.0f) + t = 0.0f; + if (t > 1.0f) + t = 1.0f; + + float ages[WAKE_TRAIL_POINTS]; + ComputeWakeAges(trail, GetTime(), ages); + + int pointsLoc = GetShaderLocation(wake->shader, "trailPoints"); + int agesLoc = GetShaderLocation(wake->shader, "trailAges"); + int countLoc = GetShaderLocation(wake->shader, "trailCount"); + int intensityLoc = GetShaderLocation(wake->shader, "wakeIntensity"); + SetShaderValueV(wake->shader, pointsLoc, trail->points, SHADER_UNIFORM_VEC2, WAKE_TRAIL_POINTS); + SetShaderValueV(wake->shader, agesLoc, ages, SHADER_UNIFORM_FLOAT, WAKE_TRAIL_POINTS); + SetShaderValue(wake->shader, countLoc, &trail->count, SHADER_UNIFORM_INT); + SetShaderValue(wake->shader, intensityLoc, &t, SHADER_UNIFORM_FLOAT); + + Rectangle box = ComputeWakeBoundingBox(trail); + + BeginShaderMode(wake->shader); + DrawRectangleRec(box, WHITE); + EndShaderMode(); +} + +void UnloadWake(Wake *wake) { + UnloadShader(wake->shader); +} diff --git a/src/appInput.c b/src/appInput.c new file mode 100644 index 0000000..0443295 --- /dev/null +++ b/src/appInput.c @@ -0,0 +1,28 @@ +#include "appCore.h" +#include + +typedef struct { + int keys[2]; +} ActionBinding; + +static const ActionBinding actionBindings[ACTION_COUNT] = { + [ACTION_FORWARD] = {{KEY_W, KEY_UP}}, + [ACTION_BACKWARD] = {{KEY_S, KEY_DOWN}}, + [ACTION_TURN_LEFT] = {{KEY_A, KEY_LEFT}}, + [ACTION_TURN_RIGHT] = {{KEY_D, KEY_RIGHT}}, + [ACTION_FIRE_PORT] = {{KEY_Q, KEY_NULL}}, + [ACTION_FIRE_STARBOARD] = {{KEY_E, KEY_NULL}}, + [ACTION_BACK] = {{KEY_ESCAPE, KEY_NULL}}, +}; + +bool IsGameActionPressed(GameAction action) { + const ActionBinding *binding = &actionBindings[action]; + return IsKeyPressed(binding->keys[0]) || + (binding->keys[1] != KEY_NULL && IsKeyPressed(binding->keys[1])); +} + +bool IsGameActionDown(GameAction action) { + const ActionBinding *binding = &actionBindings[action]; + return IsKeyDown(binding->keys[0]) || + (binding->keys[1] != KEY_NULL && IsKeyDown(binding->keys[1])); +} diff --git a/src/appLogo.c b/src/appLogo.c index 9e2e653..1c2f058 100644 --- a/src/appLogo.c +++ b/src/appLogo.c @@ -1,4 +1,5 @@ -#include "app.h" +#include "logo.h" +#include void InitLogo(LogoState *l) { SetTargetFPS(60); @@ -16,7 +17,7 @@ void InitLogo(LogoState *l) { void UpdateLogo(LogoState *l, View *currentView, bool *running) { - if (WindowShouldClose() && !IsKeyPressed(KEY_ESCAPE)) { + if (WindowShouldClose() && !IsGameActionPressed(ACTION_BACK)) { *running = false; } diff --git a/src/appMenu.c b/src/appMenu.c index 2bcc27c..4255c5b 100644 --- a/src/appMenu.c +++ b/src/appMenu.c @@ -1,49 +1,42 @@ -#include "app.h" +#include "menu.h" -void InitMenu(MenuState *m) -{ - m->menuView = MAIN; - InitMenuSettings(&m->menuSettings); - InitMenuMain(&m->menuMain); +void InitMenu(MenuState *m) { + m->menuView = MAIN; + InitMenuSettings(&m->menuSettings); + InitMenuMain(&m->menuMain); } -void UpdateMenu(MenuState *m, State *state, Flags *flags) -{ - switch (m->menuView) - { - case MAIN: - { - UpdateMenuMain(&m->menuMain, state, flags, &m->menuView); - }break; +void UpdateMenu(MenuState *m, State *state, Flags *flags) { + switch (m->menuView) { + case MAIN: + UpdateMenuMain(&m->menuMain, state, flags, &m->menuView); + break; - case SETTINGS: - { - UpdateMenuSettings(&m->menuSettings, &m->menuView, state, flags); - }break; - default: break; - } + case SETTINGS: + UpdateMenuSettings(&m->menuSettings, &m->menuView, state, flags); + break; + default: + break; + } } -void RenderMenu(MenuState *m, Font *font, const char *title, Flags *flags) -{ - switch (m->menuView) - { - case MAIN: - { - RenderMenuMain(&m->menuMain, font, title, flags); - }break; +void RenderMenu(MenuState *m, Font *font, const char *title, Flags *flags, State *state) { + switch (m->menuView) { + case MAIN: + RenderMenuMain(&m->menuMain, font, title, flags, state->gameStarted); + break; - case SETTINGS: - { - RenderMenuSettings(&m->menuSettings, flags); - } - default: break; - } + case SETTINGS: + RenderMenuSettings(&m->menuSettings, flags); + break; + + default: + break; + } } -void CleanupMenu(MenuState *m) -{ - CleanupMenuMain(&m->menuMain); +void CleanupMenu(MenuState *m) { + CleanupMenuMain(&m->menuMain); + CleanupMenuSettings(&m->menuSettings); } - diff --git a/src/appMenuMain.c b/src/appMenuMain.c index 5f28ebd..c668de1 100644 --- a/src/appMenuMain.c +++ b/src/appMenuMain.c @@ -1,209 +1,171 @@ -#include "app.h" +#include +#include "engineAssets.h" +#include "menu.h" const TextureAsset menuTextureAssets[MENU_TEX_COUNT] = { {"assets/mouse.png", 16.0f, 16.0f, 0.0f}, - {"assets/buttons.png", 288.0f/3, 96.0f/3, 0.0f}, + {"assets/buttons.png", 288.0f / 3, 96.0f / 3, 0.0f}, {"assets/menu_board.png", 128.0f, 144.0f, 0.0f}, {"assets/cloud_background.png", 576.0f, 324.0f, 0.0f}, {"assets/cloud_midground.png", 576.0f, 324.0f, 0.0f}, {"assets/cloud_foreground.png", 576.0f, 324.0f, 0.0f}}; +const SoundtrackAsset menuSoundtrackAssets[MENU_SOUNDTRACK_COUNT] = {{"assets/theme.mp3"}}; -const SoundtrackAsset menuSoundtrackAssets[MENU_SOUNDTRACK_COUNT] = { - {"assets/theme.mp3"} -}; +const SoundAsset menuSoundAssets[MENU_SOUND_COUNT] = {{"assets/button_hover.wav"}, + {"assets/button_click.wav"}}; +static void InitMenuMainAssets(MenuMain *m) { + m->assets = (Assets){.textureAssets = menuTextureAssets, + .soundAssets = menuSoundAssets, + .soundtrackAssets = menuSoundtrackAssets}; -const SoundAsset menuSoundAssets[MENU_SOUND_COUNT] = { - {"assets/button_hover.wav"}, - {"assets/button_click.wav"}}; + for (int i = 0; i < MENU_TEX_COUNT; i++) { + m->textures[i] = LoadTextureCached(m->assets.textureAssets[i].path); + } + for (int i = 0; i < MENU_SOUND_COUNT; i++) { + m->sounds[i] = LoadSoundCached(m->assets.soundAssets[i].path); + } -void InitMenuMain(MenuMain *m){ - - m->assets = (Assets){ - .textureAssets = menuTextureAssets, - .soundAssets = menuSoundAssets, - .soundtrackAssets = menuSoundtrackAssets}; - - for (int i = 0; i < MENU_TEX_COUNT; i++) - { - m->textures[i] = LoadTexture(m->assets.textureAssets[i].path); - } - - for (int i = 0; i < MENU_SOUND_COUNT; i++) - { - m->sounds[i] = LoadSound(m->assets.soundAssets[i].path); - } - - for (int i = 0; i < MENU_SOUNDTRACK_COUNT; i++) - { - m->soundtracks[i] = LoadSoundtrack(m->assets.soundtrackAssets[i].path); - } - - InitBackground(0.1f, &m->background, &m->textures[MENU_CLOUD_BACKGROUND], &m->assets.textureAssets[MENU_CLOUD_BACKGROUND]); - InitBackground(0.2f, &m->midground, &m->textures[MENU_CLOUD_MIDGROUND], &m->assets.textureAssets[MENU_CLOUD_MIDGROUND]); - InitBackground(0.3f, &m->foreground, &m->textures[MENU_CLOUD_FOREGROUND], &m->assets.textureAssets[MENU_CLOUD_FOREGROUND]); - - m->mouse = CreateMouse(0.10f, 5.0f, 10.0f, &m->textures[MENU_CURSOR_TEX]); - - m->menuScale = 4; - int windowWidth = GetScreenWidth(); - int windowHeight = GetScreenHeight(); - Vector2 windowCenter = {windowWidth / 2, windowHeight / 2}; - - int boardWidth = windowWidth / m->menuScale * 2.5f; - float boardScaleFactor = (float)boardWidth / m->assets.textureAssets[MENU_BOARD_TEX].frameWidth; - int boardHeight = (int)(m->assets.textureAssets[MENU_BOARD_TEX].frameHeight * boardScaleFactor); - - Vector2 boardSource = {128.0f, 0.0f}; - Rectangle boardDestRec = { - .x = windowCenter.x - (boardWidth / 2), - .y = windowCenter.y - (boardHeight / 2), - .width = boardWidth, - .height = boardHeight}; - - m->board = CreateImageElement(&m->textures[MENU_BOARD_TEX], &m->assets.textureAssets[MENU_BOARD_TEX], &boardSource, &boardDestRec); - - int buttonWidth = windowWidth / m->menuScale; - float buttonScaleFactor = (float)buttonWidth / m->assets.textureAssets[MENU_BIG_BUTTONS_TEX].frameWidth; - int buttonHeight = (int)(m->assets.textureAssets[MENU_BIG_BUTTONS_TEX].frameHeight * buttonScaleFactor); - int buttonSpacing = buttonHeight; - - - //Button texture dimensions - float menuButtonWidth = m->assets.textureAssets[MENU_BIG_BUTTONS_TEX].frameWidth; - float menuButtonHeight = m->assets.textureAssets[MENU_BIG_BUTTONS_TEX].frameHeight; - - //Quit Button - Vector2 quitButtonNotPressed = {0.0f, 0.0f}; - Vector2 quitButtonHover = {menuButtonWidth, 0.0f}; - Vector2 quitButtonPressed = {menuButtonWidth * 2, 0.0f}; - - Rectangle quitButtonDestRec = { - .x = windowCenter.x - (buttonWidth / 2), - .y = windowCenter.y - (buttonHeight / 2) + buttonSpacing, - .width = buttonWidth, - .height = buttonHeight - }; - - m->quitButton = CreateImageButton(NOT_CLICKED, &m->textures[MENU_BIG_BUTTONS_TEX], &m->assets.textureAssets[MENU_BIG_BUTTONS_TEX], &quitButtonNotPressed, &quitButtonPressed, &quitButtonHover, &quitButtonDestRec); - - //Play Button - Vector2 playButtonNotPressed = {0.0f, menuButtonHeight}; - Vector2 playButtonHover = {menuButtonWidth, menuButtonHeight}; - Vector2 playButtonPressed = {menuButtonWidth * 2, menuButtonHeight}; - - Rectangle playButtonDestRec = { - .x = windowCenter.x - (buttonWidth / 2), - .y = windowCenter.y - (buttonHeight / 2), - .width = buttonWidth, - .height = buttonHeight - }; - - m->playButton = CreateImageButton(NOT_CLICKED, &m->textures[MENU_BIG_BUTTONS_TEX], &m->assets.textureAssets[MENU_BIG_BUTTONS_TEX], &playButtonNotPressed, &playButtonPressed, &playButtonHover, &playButtonDestRec); - - //Settings Button - Vector2 settingsButtonNotPressed = {0.0f, menuButtonHeight * 2}; - Vector2 settingsButtonHover = {menuButtonWidth, menuButtonHeight * 2}; - Vector2 settingsButtonPressed = {menuButtonWidth * 2, menuButtonHeight * 2}; - - Rectangle settingsButtonDestRec = { - .x = windowCenter.x - (buttonWidth / 2), - .y = windowCenter.y - (buttonHeight / 2) + buttonSpacing * 2, - .width = buttonWidth, - .height = buttonHeight - }; - - m->settingsButton = CreateImageButton(NOT_CLICKED, &m->textures[MENU_BIG_BUTTONS_TEX], &m->assets.textureAssets[MENU_BIG_BUTTONS_TEX], &settingsButtonNotPressed, &settingsButtonPressed, &settingsButtonHover, &settingsButtonDestRec); - + for (int i = 0; i < MENU_SOUNDTRACK_COUNT; i++) { + m->soundtracks[i] = LoadSoundtrack(m->assets.soundtrackAssets[i].path); + } } -void UpdateMenuMain(MenuMain *m, State *state, Flags *flags, MenuView *menuView){ - for (int i = 0; i < MENU_SOUNDTRACK_COUNT; i++) - { - UpdateSoundtrack(&m->soundtracks[i], flags->soundtrackOn); - } - - if (GetFPS() >= 60) - { - SetTargetFPS(60); - } - - if (isImageButtonClicked(&m->playButton, m->sounds[HOVER_SOUND], m->sounds[CLICK_SOUND])) - { - state->running = true; - state->currentView = GAMEPLAY; - } - - if (IsKeyPressed(KEY_ESCAPE) && state->gameStarted) - { - state->currentView = GAMEPLAY; - } - - if (isImageButtonClicked(&m->settingsButton, m->sounds[HOVER_SOUND], m->sounds[CLICK_SOUND])) - { - *menuView = SETTINGS; - } - - if (isImageButtonClicked(&m->quitButton, m->sounds[HOVER_SOUND], m->sounds[CLICK_SOUND]) || (WindowShouldClose() && !IsKeyPressed(KEY_ESCAPE))) - { - state->running = false; - } - - - UpdateBackground(&m->background); - UpdateBackground(&m->midground); - UpdateBackground(&m->foreground); - UpdateMouseScreen(&m->mouse); - +static void InitMenuMainBackgrounds(MenuMain *m) { + InitBackground(0.1f, &m->background, &m->textures[MENU_CLOUD_BACKGROUND], + &m->assets.textureAssets[MENU_CLOUD_BACKGROUND]); + InitBackground(0.2f, &m->midground, &m->textures[MENU_CLOUD_MIDGROUND], + &m->assets.textureAssets[MENU_CLOUD_MIDGROUND]); + InitBackground(0.3f, &m->foreground, &m->textures[MENU_CLOUD_FOREGROUND], + &m->assets.textureAssets[MENU_CLOUD_FOREGROUND]); } -void RenderMenuMain(MenuMain *m, Font *font, const char *title, Flags *flags){ - BeginDrawing(); - ClearBackground(WHITE); - RenderBackground(&m->background); - RenderBackground(&m->midground); - RenderBackground(&m->foreground); - renderImageElement(&m->board); - renderImageButton(&m->playButton); - renderImageButton(&m->quitButton); - renderImageButton(&m->settingsButton); - int textWidth = MeasureText(title, font->baseSize); - DrawTextEx(*font, title, (Vector2){GetScreenWidth() / 2 - textWidth / 2, textWidth / 4}, (float)font->baseSize, 2, WHITE); - DrawTexturePro( - m->mouse.cursorTexture, - (Rectangle){0, 0, m->mouse.cursorTexture.width, m->mouse.cursorTexture.height}, - (Rectangle){m->mouse.screenPosition.x, m->mouse.screenPosition.y, m->mouse.cursorTexture.width, m->mouse.cursorTexture.height}, - (Vector2){0.0f, 0.0f}, - 0.0f, - WHITE); +static void InitMenuMainBoard(MenuMain *m, Vector2 windowCenter) { + int boardWidth = GetScreenWidth() / m->menuScale * 2.5f; + float boardScaleFactor = (float)boardWidth / m->assets.textureAssets[MENU_BOARD_TEX].frameWidth; + int boardHeight = (int)(m->assets.textureAssets[MENU_BOARD_TEX].frameHeight * boardScaleFactor); - if (flags->showFPS) - { - DrawFPS(10, 10); - } - - EndDrawing(); + Vector2 boardSource = {128.0f, 0.0f}; + Rectangle boardDestRec = {.x = windowCenter.x - (boardWidth / 2), + .y = windowCenter.y - (boardHeight / 2), + .width = boardWidth, + .height = boardHeight}; + m->board = + CreateImageElement(&m->textures[MENU_BOARD_TEX], &m->assets.textureAssets[MENU_BOARD_TEX], + &boardSource, &boardDestRec); } -void CleanupMenuMain(MenuMain *m){ - for (int i = 0; i < MENU_SOUND_COUNT; i++) - { - UnloadSound(m->sounds[i]); - } - for (int i = 0; i < MENU_TEX_COUNT; i++) - { - if (m->textures[i].id > 0) - { - UnloadTexture(m->textures[i]); - } - } - for (int i = 0; i < MENU_SOUNDTRACK_COUNT; i++) - { - UnloadMusicStream(m->soundtracks[i].music); - } +static void InitMenuMainButtons(MenuMain *m, Vector2 windowCenter) { + int buttonWidth = GetScreenWidth() / m->menuScale; + float buttonScaleFactor = + (float)buttonWidth / m->assets.textureAssets[MENU_BIG_BUTTONS_TEX].frameWidth; + int buttonHeight = + (int)(m->assets.textureAssets[MENU_BIG_BUTTONS_TEX].frameHeight * buttonScaleFactor); + int buttonSpacing = buttonHeight; + + m->quitButton = CreateMenuButton(&m->textures[MENU_BIG_BUTTONS_TEX], + &m->assets.textureAssets[MENU_BIG_BUTTONS_TEX], 0, 1, + buttonWidth, buttonHeight, windowCenter, buttonSpacing); + m->playButton = CreateMenuButton(&m->textures[MENU_BIG_BUTTONS_TEX], + &m->assets.textureAssets[MENU_BIG_BUTTONS_TEX], 1, 0, + buttonWidth, buttonHeight, windowCenter, buttonSpacing); + m->settingsButton = CreateMenuButton(&m->textures[MENU_BIG_BUTTONS_TEX], + &m->assets.textureAssets[MENU_BIG_BUTTONS_TEX], 2, 2, + buttonWidth, buttonHeight, windowCenter, buttonSpacing); + + Vector2 restartDimensions = {buttonWidth * 0.8f, buttonHeight * 0.4f}; + float settingsBottom = + m->settingsButton.sprite.destRec.y + m->settingsButton.sprite.destRec.height; + Vector2 restartCenter = {windowCenter.x, settingsBottom + restartDimensions.y / 2.0f + 16.0f}; + m->restartButton = initButton("Restart", restartDimensions, restartCenter); } +void InitMenuMain(MenuMain *m) { + InitMenuMainAssets(m); + InitMenuMainBackgrounds(m); + + m->mouse = CreateMouse(0.10f, 5.0f, 10.0f, &m->textures[MENU_CURSOR_TEX]); + m->menuScale = 4; + + Vector2 windowCenter = {GetScreenWidth() / 2, GetScreenHeight() / 2}; + InitMenuMainBoard(m, windowCenter); + InitMenuMainButtons(m, windowCenter); +} + +void UpdateMenuMain(MenuMain *m, State *state, Flags *flags, MenuView *menuView) { + for (int i = 0; i < MENU_SOUNDTRACK_COUNT; i++) { + UpdateSoundtrack(&m->soundtracks[i], flags->soundtrackOn); + } + + if (GetFPS() >= 60) { + SetTargetFPS(60); + } + + if (isImageButtonClicked(&m->playButton, m->sounds[HOVER_SOUND], m->sounds[CLICK_SOUND])) { + state->running = true; + state->currentView = GAMEPLAY; + } + + if (IsGameActionPressed(ACTION_BACK) && state->gameStarted) { + state->currentView = GAMEPLAY; + } + + if (state->gameStarted && isButtonClicked(&m->restartButton)) { + state->restartRequested = true; + state->currentView = GAMEPLAY; + } + + if (isImageButtonClicked(&m->settingsButton, m->sounds[HOVER_SOUND], m->sounds[CLICK_SOUND])) { + *menuView = SETTINGS; + } + + if (isImageButtonClicked(&m->quitButton, m->sounds[HOVER_SOUND], m->sounds[CLICK_SOUND]) || + (WindowShouldClose() && !IsGameActionPressed(ACTION_BACK))) { + state->running = false; + } + + UpdateBackground(&m->background); + UpdateBackground(&m->midground); + UpdateBackground(&m->foreground); + UpdateMouseScreen(&m->mouse); +} + +void RenderMenuMain(MenuMain *m, Font *font, const char *title, Flags *flags, bool gameStarted) { + BeginDrawing(); + ClearBackground(WHITE); + RenderBackground(&m->background); + RenderBackground(&m->midground); + RenderBackground(&m->foreground); + renderImageElement(&m->board); + renderImageButton(&m->playButton); + renderImageButton(&m->quitButton); + renderImageButton(&m->settingsButton); + if (gameStarted) { + renderButton(&m->restartButton); + } + + int textWidth = MeasureText(title, font->baseSize); + DrawTextEx(*font, title, (Vector2){GetScreenWidth() / 2 - textWidth / 2, textWidth / 4}, + (float)font->baseSize, 2, WHITE); + DrawTexturePro(m->mouse.cursorTexture, + (Rectangle){0, 0, m->mouse.cursorTexture.width, m->mouse.cursorTexture.height}, + (Rectangle){m->mouse.screenPosition.x, m->mouse.screenPosition.y, + m->mouse.cursorTexture.width, m->mouse.cursorTexture.height}, + (Vector2){0.0f, 0.0f}, 0.0f, WHITE); + + if (flags->showFPS) { + DrawFPS(10, 10); + } + + EndDrawing(); +} + +void CleanupMenuMain(MenuMain *m) { + for (int i = 0; i < MENU_SOUNDTRACK_COUNT; i++) { + UnloadMusicStream(m->soundtracks[i].music); + } +} diff --git a/src/appMenuMainBackground.c b/src/appMenuMainBackground.c index c7bbbe0..3ec5154 100644 --- a/src/appMenuMainBackground.c +++ b/src/appMenuMainBackground.c @@ -1,58 +1,45 @@ -#include "app.h" +#include -void InitBackground(float scrollSpeed, Background *bg, Texture2D *texture, const TextureAsset *backgroundAsset){ - bg->scrolling = 0.0f; - bg->scrollSpeed = scrollSpeed; - bg->scale = 1.0f; - bg->sprite = (Sprite){ - .texture = *texture, - .sourceRec = (Rectangle){0.0f,0.0f,backgroundAsset->frameWidth, backgroundAsset->frameHeight}, - .destRec = (Rectangle){0.0f,0.0f,(float)GetScreenWidth(),(float)GetScreenHeight()}, - .origin = (Vector2){0.0f,0.0f}, - .rotation = 0.0f, - .color = WHITE - }; +#include "menu.h" + +void InitBackground(float scrollSpeed, Background *bg, Texture2D *texture, + const TextureAsset *backgroundAsset) { + bg->scrolling = 0.0f; + bg->scrollSpeed = scrollSpeed; + bg->scale = 1.0f; + bg->sprite = (Sprite){ + .texture = *texture, + .sourceRec = + (Rectangle){0.0f, 0.0f, backgroundAsset->frameWidth, backgroundAsset->frameHeight}, + .destRec = (Rectangle){0.0f, 0.0f, (float)GetScreenWidth(), (float)GetScreenHeight()}, + .origin = (Vector2){0.0f, 0.0f}, + .rotation = 0.0f, + .color = WHITE}; } -void UpdateBackground(Background *bg){ - bg->scrolling -= bg->scrollSpeed; +void UpdateBackground(Background *bg) { + bg->scrolling -= bg->scrollSpeed; - if (bg->scrolling <= -bg->sprite.texture.width * bg->scale) { - bg->scrolling = 0; - } + if (bg->scrolling <= -bg->sprite.texture.width * bg->scale) { + bg->scrolling = 0; + } - bg->pos = (Vector2){ bg->scrolling, 0.0f }; - UpdateSpriteDestRec(&bg->sprite, &bg->pos); + bg->pos = (Vector2){bg->scrolling, 0.0f}; + UpdateSpriteDestRec(&bg->sprite, &bg->pos); } -void RenderBackground(Background *bg){ - - Sprite temp0 = bg->sprite; - float newX0 = bg->sprite.destRec.x + bg->sprite.texture.width * bg->scale * 2; - Vector2 newPos0 = {newX0, bg->sprite.destRec.y}; - UpdateSpriteDestRec(&temp0, &newPos0); - RenderSprite(&temp0); - - Sprite temp = bg->sprite; - float newX = bg->sprite.destRec.x + bg->sprite.texture.width * bg->scale; - Vector2 newPos = {newX, bg->sprite.destRec.y}; - UpdateSpriteDestRec(&temp, &newPos); - RenderSprite(&temp); - - RenderSprite(&bg->sprite); - - Sprite temp2 = bg->sprite; - float newX2 = bg->sprite.destRec.x - bg->sprite.texture.width * bg->scale; - Vector2 newPos2 = {newX2, bg->sprite.destRec.y}; - UpdateSpriteDestRec(&temp2, &newPos2); - RenderSprite(&temp2); - - Sprite temp3 = bg->sprite; - float newX3 = bg->sprite.destRec.x - bg->sprite.texture.width * bg->scale * 2; - Vector2 newPos3 = {newX3, bg->sprite.destRec.y}; - UpdateSpriteDestRec(&temp3, &newPos3); - RenderSprite(&temp3); - - +static void RenderBackgroundTile(Background *bg, float tileOffset) { + Sprite tile = bg->sprite; + float x = bg->sprite.destRec.x + bg->sprite.texture.width * bg->scale * tileOffset; + Vector2 pos = {x, bg->sprite.destRec.y}; + UpdateSpriteDestRec(&tile, &pos); + RenderSprite(&tile); } +void RenderBackground(Background *bg) { + RenderBackgroundTile(bg, 2.0f); + RenderBackgroundTile(bg, 1.0f); + RenderSprite(&bg->sprite); + RenderBackgroundTile(bg, -1.0f); + RenderBackgroundTile(bg, -2.0f); +} diff --git a/src/appMenuSettings.c b/src/appMenuSettings.c index c7ea923..bd629ea 100644 --- a/src/appMenuSettings.c +++ b/src/appMenuSettings.c @@ -1,67 +1,61 @@ -#include "app.h" +#include -void InitMenuSettings(MenuSettings *s) -{ - int windowWidth = GetScreenWidth(); - int windowHeight = GetScreenHeight(); - Vector2 windowCenter = {windowWidth / 2, windowHeight / 2}; +#include "menu.h" - Vector2 standardButtonDimensions = {100, 50}; - int standardSpacing = standardButtonDimensions.y / 2; +void InitMenuSettings(MenuSettings *s) { + Vector2 windowCenter = {GetScreenWidth() / 2, GetScreenHeight() / 2}; - Vector2 button1Pos = {windowCenter.x, windowCenter.y}; - Vector2 button2Pos = {windowCenter.x, windowCenter.y + standardButtonDimensions.y * 1 + standardSpacing * 1}; - Vector2 button3Pos = {windowCenter.x, windowCenter.y + standardButtonDimensions.y * 2 + standardSpacing * 2}; + Vector2 standardButtonDimensions = {100, 50}; + int standardSpacing = standardButtonDimensions.y / 2; - s->showFPS = initButton("showFPS", standardButtonDimensions, button1Pos); - s->limitFPS = initButton("limitFPS", standardButtonDimensions, button2Pos); - s->soundtrackOn = initButton("soundtrackOn", standardButtonDimensions, button3Pos); + Vector2 button1Pos = {windowCenter.x, windowCenter.y}; + Vector2 button2Pos = {windowCenter.x, + windowCenter.y + standardButtonDimensions.y * 1 + standardSpacing * 1}; + Vector2 button3Pos = {windowCenter.x, + windowCenter.y + standardButtonDimensions.y * 2 + standardSpacing * 2}; + + s->showFPS = initButton("showFPS", standardButtonDimensions, button1Pos); + s->limitFPS = initButton("limitFPS", standardButtonDimensions, button2Pos); + s->soundtrackOn = initButton("soundtrackOn", standardButtonDimensions, button3Pos); } -void UpdateMenuSettings(MenuSettings *s, MenuView *menuView, State* state, Flags *flags) -{ +void UpdateMenuSettings(MenuSettings *s, MenuView *menuView, State *state, Flags *flags) { + if (isButtonClicked(&s->showFPS)) { + flags->showFPS = !flags->showFPS; + } - if(isButtonClicked(&s->showFPS)){ - flags->showFPS = !flags->showFPS; - } + if (isButtonClicked(&s->limitFPS)) { + flags->limitFPS = !flags->limitFPS; + } - if(isButtonClicked(&s->limitFPS)){ - flags->limitFPS = !flags->limitFPS; - } + if (isButtonClicked(&s->soundtrackOn)) { + flags->soundtrackOn = !flags->soundtrackOn; + } - if(isButtonClicked(&s->soundtrackOn)){ - flags->soundtrackOn = !flags->soundtrackOn; - } + if (IsGameActionPressed(ACTION_BACK)) { + *menuView = MAIN; + } - if (IsKeyPressed(KEY_ESCAPE)){ - *menuView = MAIN; - } - - if (WindowShouldClose() && !IsKeyPressed(KEY_ESCAPE)){ - state->running = false; - } - - + if (WindowShouldClose() && !IsGameActionPressed(ACTION_BACK)) { + state->running = false; + } } -void RenderMenuSettings(MenuSettings *s, Flags *flags) -{ - BeginDrawing(); - ClearBackground(BLACK); +void RenderMenuSettings(MenuSettings *s, Flags *flags) { + BeginDrawing(); + ClearBackground(BLACK); - renderButton(&s->showFPS); - renderButton(&s->limitFPS); - renderButton(&s->soundtrackOn); + renderButton(&s->showFPS); + renderButton(&s->limitFPS); + renderButton(&s->soundtrackOn); - if (flags->showFPS){ - DrawFPS(10,10); - } + if (flags->showFPS) { + DrawFPS(10, 10); + } - EndDrawing(); - + EndDrawing(); } -void ClenupMenuSettings(MenuSettings *s){ - +void CleanupMenuSettings(MenuSettings *s) { + (void)s; } - diff --git a/src/appState.c b/src/appState.c index b5114f8..3dba2c3 100644 --- a/src/appState.c +++ b/src/appState.c @@ -1,8 +1,8 @@ #include "app.h" void OnEscapeChangeView(App *app, View view) -{ - if (IsKeyPressed(KEY_ESCAPE)) +{ + if (IsGameActionPressed(ACTION_BACK)) { app->state.currentView = view; } @@ -10,8 +10,16 @@ void OnEscapeChangeView(App *app, View view) void OnWindowClosedStoppApp(App *app) { - if (WindowShouldClose() && !IsKeyPressed(KEY_ESCAPE)) + if (WindowShouldClose() && !IsGameActionPressed(ACTION_BACK)) { app->state.running = false; } } + +void OnBoolChangeView(bool flag, App *app, View view) +{ + if (flag) + { + app->state.currentView = view; + } +} diff --git a/src/engineAnimation.c b/src/engineAnimation.c index cf6cd30..8dd39bd 100644 --- a/src/engineAnimation.c +++ b/src/engineAnimation.c @@ -1,17 +1,17 @@ -#include "engine.h" +#include "engineAnimation.h" void UpdateAnimation(Animation *animation, float deltaTime) { - animation->animTimer += deltaTime; - float frameTime = 1.0f / animation->framesSpeed; + animation->animTimer += deltaTime; + float frameTime = 1.0f / animation->framesSpeed; - if (animation->animTimer < frameTime) { - return; - } + if (animation->animTimer < frameTime) { + return; + } - animation->animTimer -= frameTime; - animation->currentFrame++; + animation->animTimer -= frameTime; + animation->currentFrame++; - if (animation->currentFrame >= animation->maxFrame) { - animation->currentFrame = 0; - } + if (animation->currentFrame >= animation->maxFrame) { + animation->currentFrame = 0; + } } diff --git a/src/engineArray.c b/src/engineArray.c index fb51f39..4d4bd02 100644 --- a/src/engineArray.c +++ b/src/engineArray.c @@ -1,4 +1,5 @@ -#include "engine.h" +#include "engineArray.h" +#include Array* CreateArray(unsigned int initCapacity) { diff --git a/src/engineAssets.c b/src/engineAssets.c new file mode 100644 index 0000000..8a3d863 --- /dev/null +++ b/src/engineAssets.c @@ -0,0 +1,78 @@ +#include "engineAssets.h" +#include +#include + +#define ASSET_CACHE_CAPACITY 64 +#define ASSET_PATH_MAX 128 + +typedef struct { + char path[ASSET_PATH_MAX]; + Texture2D texture; +} CachedTexture; + +typedef struct { + char path[ASSET_PATH_MAX]; + Sound sound; +} CachedSound; + +static CachedTexture textureCache[ASSET_CACHE_CAPACITY]; +static int textureCacheCount = 0; + +static CachedSound soundCache[ASSET_CACHE_CAPACITY]; +static int soundCacheCount = 0; + +Texture2D LoadTextureCached(const char *path) { + for (int i = 0; i < textureCacheCount; i++) { + if (strcmp(textureCache[i].path, path) == 0) { + return textureCache[i].texture; + } + } + + Texture2D texture = LoadTexture(path); + + if (textureCacheCount >= ASSET_CACHE_CAPACITY) { + TraceLog(LOG_WARNING, "ASSETS: Texture cache full, not caching %s", path); + return texture; + } + + CachedTexture *entry = &textureCache[textureCacheCount++]; + strncpy(entry->path, path, ASSET_PATH_MAX - 1); + entry->path[ASSET_PATH_MAX - 1] = '\0'; + entry->texture = texture; + + return texture; +} + +Sound LoadSoundCached(const char *path) { + for (int i = 0; i < soundCacheCount; i++) { + if (strcmp(soundCache[i].path, path) == 0) { + return soundCache[i].sound; + } + } + + Sound sound = LoadSound(path); + + if (soundCacheCount >= ASSET_CACHE_CAPACITY) { + TraceLog(LOG_WARNING, "ASSETS: Sound cache full, not caching %s", path); + return sound; + } + + CachedSound *entry = &soundCache[soundCacheCount++]; + strncpy(entry->path, path, ASSET_PATH_MAX - 1); + entry->path[ASSET_PATH_MAX - 1] = '\0'; + entry->sound = sound; + + return sound; +} + +void UnloadCachedAssets(void) { + for (int i = 0; i < textureCacheCount; i++) { + UnloadTexture(textureCache[i].texture); + } + textureCacheCount = 0; + + for (int i = 0; i < soundCacheCount; i++) { + UnloadSound(soundCache[i].sound); + } + soundCacheCount = 0; +} diff --git a/src/engineCamera.c b/src/engineCamera.c index eb93876..b0d4a91 100644 --- a/src/engineCamera.c +++ b/src/engineCamera.c @@ -1,13 +1,8 @@ -#include "engine.h" +#include "engineCamera.h" +#include -Camera2D* GetCamera(void){ - static Camera2D camera; - return &camera; -} - -Rectangle GetCameraRectangle(void) +Rectangle GetCameraRectangle(Camera2D *camera) { - Camera2D *camera = GetCamera(); float screenWidth = GetScreenWidth(); float screenHeight = GetScreenHeight(); @@ -38,11 +33,10 @@ Rectangle GetCameraRectangle(void) }; } -void CreateCamera(Vector2 playerPosition, float initZoom){ - Camera2D* camera = GetCamera(); +Camera2D CreateCamera(Vector2 playerPosition, float initZoom){ int windowWidth = GetScreenWidth(); int windowHeight = GetScreenHeight(); - *camera = (Camera2D){ + return (Camera2D){ .offset = (Vector2){windowWidth / 2.0f, windowHeight / 2.0f}, .target = playerPosition, .rotation = 0.0f, @@ -50,19 +44,17 @@ void CreateCamera(Vector2 playerPosition, float initZoom){ }; } -void UpdateCameraTarget(Vector2 *position, const float smoothness){ - Camera2D* camera = GetCamera(); +void UpdateCameraTarget(Camera2D *camera, Vector2 *position, const float smoothness){ camera->target = Vector2Lerp(camera->target, *position, smoothness * GetFrameTime()); } -void UpdateCameraZoom(const float *scaleFactor, const float *minZoom, const float *maxZoom){ - Camera2D *camera = GetCamera(); +void UpdateCameraZoom(Camera2D *camera, const float *scaleFactor, const float *minZoom, const float *maxZoom){ float targetZoom = Clamp(camera->zoom * *scaleFactor, *minZoom, *maxZoom); camera->zoom = Lerp(camera->zoom, targetZoom, 0.1f); } -void UpdateCamera2D(Vector2* position, Mouse* mouse){ - UpdateCameraTarget(position, 5.0f); - UpdateCameraZoom(&mouse->scaleFactor, &mouse->minZoom, &mouse->maxZoom); +void UpdateCamera2D(Camera2D *camera, Vector2* position, Mouse* mouse){ + UpdateCameraTarget(camera, position, 5.0f); + UpdateCameraZoom(camera, &mouse->scaleFactor, &mouse->minZoom, &mouse->maxZoom); } diff --git a/src/engineConfig.c b/src/engineConfig.c index 76f7ce4..1d98b75 100644 --- a/src/engineConfig.c +++ b/src/engineConfig.c @@ -1,56 +1,57 @@ #define RINI_IMPLEMENTATION #define RINI_VALUE_DELIMITER '=' -#include "engine.h" +#include "engineConfig.h" -void InitConfig(Config *config, const char* path) { - config->rini = rini_load_config(path); - if (config->rini.count == 0) { - printf("Warning: Loaded config has no entries. Path: %s\n", config->path); - } +#include + +void InitConfig(Config *config, const char *path) { + config->rini = rini_load(path); + if (config->rini.count == 0) { + printf("Warning: Loaded config has no entries. Path: %s\n", config->path); + } } int GetConfigInt(Config *config, const char *key) { - return rini_get_config_value_fallback(config->rini, key, 0); + return rini_get_value_fallback(config->rini, key, 0); } int GetConfigIntOrDefault(Config *config, const char *key, int default_value) { - return rini_get_config_value_fallback(config->rini, key, default_value); + return rini_get_value_fallback(config->rini, key, default_value); } -const char* GetConfigString(Config *config, const char *key) { - return rini_get_config_value_text(config->rini, key); +const char *GetConfigString(Config *config, const char *key) { + return rini_get_value_text(config->rini, key); } -const char* GetConfigStringOrDefault(Config *config, const char *key, const char *default_value) { - return rini_get_config_value_text_fallback(config->rini, key, default_value); +const char *GetConfigStringOrDefault(Config *config, const char *key, const char *default_value) { + return rini_get_value_text_fallback(config->rini, key, default_value); } -const char* GetConfigDescription(Config *config, const char *key) { - return rini_get_config_value_description(config->rini, key); +const char *GetConfigDescription(Config *config, const char *key) { + return rini_get_value_description(config->rini, key); } int SetConfigInt(Config *config, const char *key, int value, const char *desc) { - return rini_set_config_value(&config->rini, key, value, desc); + return rini_set_value(&config->rini, key, value, desc); } int SetConfigString(Config *config, const char *key, const char *text, const char *desc) { - return rini_set_config_value_text(&config->rini, key, text, desc); + return rini_set_value_text(&config->rini, key, text, desc); } int SetConfigDescription(Config *config, const char *key, const char *desc) { - return rini_set_config_value_description(&config->rini, key, desc); + return rini_set_value_description(&config->rini, key, desc); } int AddCommentLine(Config *config, const char *comment) { - return rini_set_config_comment_line(&config->rini, comment); + return rini_set_comment_line(&config->rini, comment); } void SaveConfig(Config *config) { - rini_save_config(config->rini, config->path); + rini_save(config->rini, config->path); } void CleanUpConfig(Config *config) { - rini_unload_config(&config->rini); + rini_unload(&config->rini); } - diff --git a/src/engineFont.c b/src/engineFont.c index c7acecf..04e1099 100644 --- a/src/engineFont.c +++ b/src/engineFont.c @@ -1,11 +1,10 @@ -#include "engine.h" +#include "engineFont.h" -void InitFont(Font *font, const char* path){ - *font = LoadFont(path); - SetTextureFilter(font->texture, TEXTURE_FILTER_POINT); +void InitFont(Font *font, const char *path) { + *font = LoadFont(path); + SetTextureFilter(font->texture, TEXTURE_FILTER_POINT); } -void CleanupFont(Font *font){ - UnloadFont(*font); +void CleanupFont(Font *font) { + UnloadFont(*font); } - diff --git a/src/engineGamegui.c b/src/engineGamegui.c deleted file mode 100644 index e1e4b29..0000000 --- a/src/engineGamegui.c +++ /dev/null @@ -1,72 +0,0 @@ -#include "engine.h" - -ImageButton CreateImageButton(ButtonState state, Texture2D *texture, const TextureAsset *asset, Vector2 *source, Vector2 *clicked, Vector2 *hover, Rectangle *destRec){ - return (ImageButton){ - .sprite = { - .texture = *texture, - .sourceRec = (Rectangle){ source->x, source->y, asset->frameWidth, asset->frameHeight}, - .destRec = *destRec, - .frameSize = (Vector2){asset->frameWidth, asset->frameHeight}, - .origin = (Vector2){0.0f, 0.0f}, - .rotation = 0.0f, - .color = WHITE - }, - .notClicked = (Vector2){source->x, source->y}, - .hover = *hover, - .clicked = *clicked - }; -} - -ImageElement CreateImageElement(Texture2D *texture, const TextureAsset *asset, Vector2 *source, Rectangle *destRec){ - return (ImageElement){ - .sprite = { - .texture = *texture, - .sourceRec = (Rectangle){ source->x, source->y, asset->frameWidth, asset->frameHeight}, - .destRec = *destRec, - .frameSize = (Vector2){asset->frameWidth, asset->frameHeight}, - .origin = (Vector2){0.0f, 0.0f}, - .rotation = 0.0f, - .color = WHITE - } - }; -} - -int isImageButtonClicked(ImageButton *b, Sound soundHover, Sound soundClick){ - if (CheckCollisionPointRec(GetMousePosition(), b->sprite.destRec)) { - - if (!b->isHovered) { - PlaySound(soundHover); - b->isHovered = true; - UpdateSpriteSourceRec(&b->sprite, &b->hover); - } - - if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)){ - if (!b->isClicked) { - PlaySound(soundClick); - b->isClicked = true; - UpdateSpriteSourceRec(&b->sprite, &b->clicked); - } - } - if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) - { - b->isClicked = false; - b->isHovered = false; - UpdateSpriteSourceRec(&b->sprite, &b->notClicked); - return true; - } - } else { - UpdateSpriteSourceRec(&b->sprite, &b->notClicked); - b->isClicked = false; - b->isHovered = false; - } - return false; -} - -void renderImageButton(ImageButton *b) { - RenderSprite(&b->sprite); -} - -void renderImageElement(ImageElement *e){ - RenderSprite(&e->sprite); -} - diff --git a/src/engineGui.c b/src/engineGui.c index 6b9454e..a6eaf01 100644 --- a/src/engineGui.c +++ b/src/engineGui.c @@ -1,23 +1,104 @@ #define RAYGUI_IMPLEMENTATION -#include "engine.h" +#include "engineGui.h" -Button initButton(char *text, Vector2 dimensions, Vector2 position) -{ - Button b; +#include - b.rec = (Rectangle){position.x - dimensions.x / 2, position.y - dimensions.y / 2, dimensions.x, dimensions.y}; - b.text = text; - return b; +ImageButton CreateImageButton(Texture2D *texture, const TextureAsset *asset, Vector2 *source, + Vector2 *clicked, Vector2 *hover, Rectangle *destRec) { + return (ImageButton){.sprite = {.texture = *texture, + .sourceRec = (Rectangle){source->x, source->y, asset->frameWidth, + asset->frameHeight}, + .destRec = *destRec, + .frameSize = (Vector2){asset->frameWidth, asset->frameHeight}, + .origin = (Vector2){0.0f, 0.0f}, + .rotation = 0.0f, + .color = WHITE}, + .notClicked = (Vector2){source->x, source->y}, + .hover = *hover, + .clicked = *clicked}; } -int isButtonClicked(Button *b) -{ - return (CheckCollisionPointRec(GetMousePosition(), b->rec) && IsMouseButtonPressed(MOUSE_LEFT_BUTTON)); +ImageElement CreateImageElement(Texture2D *texture, const TextureAsset *asset, Vector2 *source, + Rectangle *destRec) { + return (ImageElement){.sprite = {.texture = *texture, + .sourceRec = (Rectangle){source->x, source->y, asset->frameWidth, + asset->frameHeight}, + .destRec = *destRec, + .frameSize = (Vector2){asset->frameWidth, asset->frameHeight}, + .origin = (Vector2){0.0f, 0.0f}, + .rotation = 0.0f, + .color = WHITE}}; } -void renderButton(Button *b) -{ - GuiButton(b->rec, b->text); +int isImageButtonClicked(ImageButton *b, Sound soundHover, Sound soundClick) { + if (CheckCollisionPointRec(GetMousePosition(), b->sprite.destRec)) { + if (!b->isHovered) { + PlaySound(soundHover); + b->isHovered = true; + UpdateSpriteSourceRec(&b->sprite, &b->hover); + } + + if (IsMouseButtonDown(MOUSE_LEFT_BUTTON)) { + if (!b->isClicked) { + PlaySound(soundClick); + b->isClicked = true; + UpdateSpriteSourceRec(&b->sprite, &b->clicked); + } + } + if (IsMouseButtonReleased(MOUSE_LEFT_BUTTON)) { + b->isClicked = false; + b->isHovered = false; + UpdateSpriteSourceRec(&b->sprite, &b->notClicked); + return true; + } + } else { + UpdateSpriteSourceRec(&b->sprite, &b->notClicked); + b->isClicked = false; + b->isHovered = false; + } + return false; } +ImageButton CreateMenuButton(Texture2D *texture, const TextureAsset *asset, int textureRow, + int screenRank, int buttonWidth, int buttonHeight, + Vector2 windowCenter, int buttonSpacing) { + float menuButtonWidth = asset->frameWidth; + float menuButtonHeight = asset->frameHeight; + + Vector2 notClicked = {0.0f, menuButtonHeight * textureRow}; + Vector2 hover = {menuButtonWidth, menuButtonHeight * textureRow}; + Vector2 clicked = {menuButtonWidth * 2, menuButtonHeight * textureRow}; + + Rectangle destRec = {.x = windowCenter.x - (buttonWidth / 2), + .y = windowCenter.y - (buttonHeight / 2) + buttonSpacing * screenRank, + .width = buttonWidth, + .height = buttonHeight}; + + return CreateImageButton(texture, asset, ¬Clicked, &clicked, &hover, &destRec); +} + +void renderImageButton(ImageButton *b) { + RenderSprite(&b->sprite); +} + +void renderImageElement(ImageElement *e) { + RenderSprite(&e->sprite); +} + +Button initButton(char *text, Vector2 dimensions, Vector2 position) { + Button b; + b.rec = (Rectangle){position.x - dimensions.x / 2, position.y - dimensions.y / 2, dimensions.x, + dimensions.y}; + b.text = text; + return b; +} + +int isButtonClicked(Button *b) { + return (CheckCollisionPointRec(GetMousePosition(), b->rec) && + IsMouseButtonPressed(MOUSE_LEFT_BUTTON)); +} + +void renderButton(Button *b) { + GuiButton(b->rec, b->text); +} diff --git a/src/engineInput.c b/src/engineInput.c index ebd4009..1532919 100644 --- a/src/engineInput.c +++ b/src/engineInput.c @@ -1,4 +1,6 @@ -#include "engine.h" +#include "engineInput.h" +#include +#include Mouse CreateMouse(float zoomSpeed, float minZoom, float maxZoom, Texture2D *texture){ HideCursor(); @@ -15,8 +17,7 @@ void UpdateMouseScreen(Mouse *mouse){ } -void UpdateMouse(Mouse *mouse) { - Camera2D* camera = GetCamera(); +void UpdateMouse(Mouse *mouse, Camera2D *camera) { Vector2 pos = GetMousePosition(); mouse->screenPosition = pos; mouse->worldPosition = GetScreenToWorld2D(pos, *camera); diff --git a/src/engineObject.c b/src/engineObject.c index 3dbe7eb..8e57452 100644 --- a/src/engineObject.c +++ b/src/engineObject.c @@ -1,4 +1,10 @@ -#include "engine.h" +#include "engineArray.h" +#include "enginePhysics.h" +#include "engineRectangle.h" +#include "engineVector.h" +#include +#include +#include ObjectArray CreateObjectArray(const Object *prototype, const size_t count) { ObjectArray objects = {0}; diff --git a/src/enginePhysics.c b/src/enginePhysics.c index 20873b8..c072266 100644 --- a/src/enginePhysics.c +++ b/src/enginePhysics.c @@ -1,43 +1,49 @@ -#include "engine.h" +#include "enginePhysics.h" -Vector2 GetDirectionBetweenTwoVectors(Vector2 vec1, Vector2 vec2) -{ - Vector2 dir = {vec2.x - vec1.x, vec2.y - vec1.y}; - dir = Vector2Normalize(dir); - return dir; +#include + +#include "engineVector.h" + +void UpdatePhysicsDirection(Vector2 *direction, Vector2 vector) { + *direction = Vector2Normalize(vector); } -float GetDistanceBetweenTwoVectors(Vector2 vec1, Vector2 vec2) -{ - float dx = vec1.x - vec2.x; - float dy = vec1.y - vec2.y; - return sqrtf(dx * dx + dy * dy); +void UpdatePhysicsVelocity(Vector2 *velocity, Vector2 *direction, float *speed) { + *velocity = Vector2Scale(*direction, *speed * GetFrameTime()); } -void UpdatePhysicsDirection(Vector2 *direction, Vector2 vector) -{ - *direction = Vector2Normalize(vector); +void UpdatePhysicsPosition(Vector2 *position, Vector2 *velocity) { + *position = Vector2Add(*position, *velocity); } -void UpdatePhysicsVelocity(Vector2 *velocity, Vector2 *direction, float *speed) -{ - *velocity = Vector2Scale(*direction, *speed * GetFrameTime()); +void UpdatePhysicsSpeed(float *speed, float *newSpeed) { + *speed = *newSpeed; } -void UpdatePhysicsPosition(Vector2 *position, Vector2 *velocity) -{ - *position = Vector2Add(*position, *velocity); +void UpdatePhysics(Physics *physics, Vector2 direction) { + UpdatePhysicsDirection(&physics->direction, direction); + UpdatePhysicsVelocity(&physics->velocity, &physics->direction, &physics->speed); + UpdatePhysicsPosition(&physics->position, &physics->velocity); } -void UpdatePhysicsSpeed(float *speed, float *newSpeed) -{ - *speed = *newSpeed; +float GetActualSpeed(Physics *physics) { + return physics->speed; } -void UpdatePhysics(Physics *physics, Vector2 direction) -{ - UpdatePhysicsDirection(&physics->direction, direction); - UpdatePhysicsVelocity(&physics->velocity, &physics->direction, &physics->speed); - UpdatePhysicsPosition(&physics->position, &physics->velocity); -} +CircleCollision CheckCircleCollision(Vector2 posA, float radiusA, Vector2 posB, float radiusB) { + CircleCollision result = {0}; + result.overlapping = CheckCollisionCircles(posA, radiusA, posB, radiusB); + if (!result.overlapping) { + return result; + } + + float dist = GetDistanceBetweenTwoVectors(posA, posB); + result.hasDirection = dist > 0.0f; + if (result.hasDirection) { + result.direction = GetDirectionBetweenTwoVectors(posB, posA); + result.overlap = (radiusA + radiusB) - dist; + } + + return result; +} diff --git a/src/enginePostfx.c b/src/enginePostfx.c index 04776aa..f920a9b 100644 --- a/src/enginePostfx.c +++ b/src/enginePostfx.c @@ -1,36 +1,53 @@ -#include "engine.h" +#include "enginePostfx.h" -PostProcessing CreatePostFX(const char* shaderPath){ - PostProcessing postFX = {0}; - postFX.shader = LoadShader(NULL, shaderPath); +#include - if (postFX.shader.id == 0) { - TraceLog(LOG_ERROR, "Failed to load shader: %s", shaderPath); - return postFX; - } +PostProcessing CreatePostFX(const char *shaderPath) { + PostProcessing postFX = {0}; + postFX.shader = LoadShader(NULL, shaderPath); - postFX.strength = 0.0f; - postFX.resolutionLoc = GetShaderLocation(postFX.shader, "resolution"); - postFX.strengthLoc = GetShaderLocation(postFX.shader, "strength"); - Vector2 resolution = { (float)GetScreenWidth(), (float)GetScreenHeight() }; - SetShaderValue(postFX.shader, postFX.resolutionLoc, &resolution, SHADER_UNIFORM_VEC2); + if (postFX.shader.id == 0) { + TraceLog(LOG_ERROR, "Failed to load shader: %s", shaderPath); return postFX; + } + + postFX.strength = 0.0f; + postFX.time = 0.0f; + postFX.resolutionLoc = GetShaderLocation(postFX.shader, "resolution"); + postFX.strengthLoc = GetShaderLocation(postFX.shader, "strength"); + postFX.timeLoc = GetShaderLocation(postFX.shader, "time"); + Vector2 resolution = {(float)GetScreenWidth(), (float)GetScreenHeight()}; + SetShaderValue(postFX.shader, postFX.resolutionLoc, &resolution, SHADER_UNIFORM_VEC2); + return postFX; } -void UpdatePostFX(PostProcessing *postFX){ - if (postFX->shader.id == 0) return; - SetShaderValue(postFX->shader, postFX->strengthLoc, &postFX->strength, SHADER_UNIFORM_FLOAT); - Vector2 resolution = { (float)GetScreenWidth(), (float)GetScreenHeight() }; - SetShaderValue(postFX->shader, postFX->resolutionLoc, &resolution, SHADER_UNIFORM_VEC2); +void UpdatePostFX(PostProcessing *postFX) { + if (postFX->shader.id == 0) + return; + SetShaderValue(postFX->shader, postFX->strengthLoc, &postFX->strength, SHADER_UNIFORM_FLOAT); + Vector2 resolution = {(float)GetScreenWidth(), (float)GetScreenHeight()}; + SetShaderValue(postFX->shader, postFX->resolutionLoc, &resolution, SHADER_UNIFORM_VEC2); + if (postFX->timeLoc != -1) { + postFX->time += GetFrameTime(); + SetShaderValue(postFX->shader, postFX->timeLoc, &postFX->time, SHADER_UNIFORM_FLOAT); + } } -void RenderPostFX(PostProcessing *postFX, RenderTexture2D *target){ - BeginShaderMode(postFX->shader); - DrawTextureRec(target->texture, (Rectangle){ 0, 0, target->texture.width, -target->texture.height }, (Vector2){ 0, 0 }, WHITE); - EndShaderMode(); +void UpdateShaders(PostProcessing *postFX, int count) { + for (int i = 0; i < count; i++) { + UpdatePostFX(&postFX[i]); + } } -void UnloadPostFX(PostProcessing *postFX){ - if (postFX->shader.id == 0) return; - UnloadShader(postFX->shader); +void RenderPostFX(PostProcessing *postFX, RenderTexture2D *target) { + BeginShaderMode(postFX->shader); + DrawTextureRec(target->texture, (Rectangle){0, 0, target->texture.width, -target->texture.height}, + (Vector2){0, 0}, WHITE); + EndShaderMode(); +} + +void UnloadPostFX(PostProcessing *postFX) { + if (postFX->shader.id == 0) + return; + UnloadShader(postFX->shader); } diff --git a/src/engineRectangle.c b/src/engineRectangle.c index d581cb7..e9fd2e0 100644 --- a/src/engineRectangle.c +++ b/src/engineRectangle.c @@ -1,40 +1,36 @@ -#include "engine.h" +#include "engineRectangle.h" -Vector2 GetPositionInsideRectangle(Rectangle rec) -{ - int randX = GetRandomValue(rec.x, rec.width); - int randY = GetRandomValue(rec.y, rec.height); - Vector2 randVector = {randX, randY}; - return randVector; +Vector2 GetPositionInsideRectangle(Rectangle rec) { + int randX = GetRandomValue(rec.x, rec.width); + int randY = GetRandomValue(rec.y, rec.height); + return (Vector2){randX, randY}; } -Vector2 GetPositionOutsideRectangle(Rectangle rec, int min, int max) -{ - int side = GetRandomValue(0, 3); - Vector2 pos = {0}; +Vector2 GetPositionOutsideRectangle(Rectangle rec, int min, int max) { + int side = GetRandomValue(0, 3); + Vector2 pos = {0}; - switch (side) - { - case 0: // left - pos.x = rec.x - GetRandomValue(min, max); - pos.y = GetRandomValue((int)rec.y, (int)(rec.y + rec.height)); - break; + switch (side) { + case 0: + pos.x = rec.x - GetRandomValue(min, max); + pos.y = GetRandomValue((int)rec.y, (int)(rec.y + rec.height)); + break; - case 1: // right - pos.x = rec.x + rec.width + GetRandomValue(min, max); - pos.y = GetRandomValue((int)rec.y, (int)(rec.y + rec.height)); - break; + case 1: + pos.x = rec.x + rec.width + GetRandomValue(min, max); + pos.y = GetRandomValue((int)rec.y, (int)(rec.y + rec.height)); + break; - case 2: // top - pos.x = GetRandomValue((int)rec.x, (int)(rec.x + rec.width)); - pos.y = rec.y - GetRandomValue(min, max); - break; + case 2: + pos.x = GetRandomValue((int)rec.x, (int)(rec.x + rec.width)); + pos.y = rec.y - GetRandomValue(min, max); + break; - case 3: // bottom - pos.x = GetRandomValue((int)rec.x, (int)(rec.x + rec.width)); - pos.y = rec.y + rec.height + GetRandomValue(min, max); - break; - } + case 3: + pos.x = GetRandomValue((int)rec.x, (int)(rec.x + rec.width)); + pos.y = rec.y + rec.height + GetRandomValue(min, max); + break; + } - return pos; + return pos; } diff --git a/src/engineSoundtrack.c b/src/engineSoundtrack.c index f46dcc7..df23157 100644 --- a/src/engineSoundtrack.c +++ b/src/engineSoundtrack.c @@ -1,4 +1,4 @@ -#include "engine.h" +#include "engineSoundtrack.h" Soundtrack LoadSoundtrack(const char* musicPath){ Soundtrack soundtrack = { diff --git a/src/engineSprite.c b/src/engineSprite.c index b72962d..5e90945 100644 --- a/src/engineSprite.c +++ b/src/engineSprite.c @@ -1,42 +1,45 @@ -#include "engine.h" +#include "engineSprite.h" +#include -void UpdateSpriteSourceRec(Sprite *sprite, const Vector2 *vector) -{ - sprite->sourceRec.x = vector->x; - sprite->sourceRec.y = vector->y; +void UpdateSpriteSourceRec(Sprite *sprite, const Vector2 *vector) { + sprite->sourceRec.x = vector->x; + sprite->sourceRec.y = vector->y; } -void UpdateSpriteDestRec(Sprite *sprite, const Vector2 *vector) -{ - sprite->destRec.x = vector->x; - sprite->destRec.y = vector->y; +void UpdateSpriteDestRec(Sprite *sprite, const Vector2 *vector) { + sprite->destRec.x = vector->x; + sprite->destRec.y = vector->y; } -void UpdateSpriteRotation(Sprite *sprite, float rotation) -{ - sprite->rotation = Lerp(sprite->rotation, rotation, 0.1f); +static float ShortestAngleDifference(float from, float to) { + float diff = fmodf(to - from, 360.0f); + if (diff < -180.0f) + diff += 360.0f; + else if (diff > 180.0f) + diff -= 360.0f; + return diff; } -void RenderSprite(const Sprite *sprite) -{ - DrawTexturePro( - sprite->texture, - sprite->sourceRec, - sprite->destRec, - sprite->origin, - sprite->rotation, - sprite->color); +void UpdateSpriteRotation(Sprite *sprite, float rotation) { + float diff = ShortestAngleDifference(sprite->rotation, rotation); + sprite->rotation += diff * 0.1f; } -void PrintSprite(const Sprite *sprite) -{ - printf("Sprite:\n"); - printf(" Sprite.texture: id = %d\n", sprite->texture.id); - printf(" Sprite.sourceRec: x = %f, y = %f, width = %f, height = %f\n", sprite->sourceRec.x, sprite->sourceRec.y, sprite->sourceRec.width, sprite->sourceRec.height); - printf(" Sprite.destRec: x = %f, y = %f, width = %f, height = %f\n", sprite->destRec.x, sprite->destRec.y, sprite->destRec.width, sprite->destRec.height); - printf(" Sprite.frameSize: x = %f, y = %f\n", sprite->frameSize.x, sprite->frameSize.y); - printf(" Sprite.origin: x = %f, y = %f\n", sprite->origin.x, sprite->origin.y); - printf(" Sprite.rotation: %f\n", sprite->rotation); - printf(" Sprite.color: r = %d, g = %d, b = %d, a = %d\n", sprite->color.r, sprite->color.g, sprite->color.b, sprite->color.a); +void UpdateSpriteRotationTowards(Sprite *sprite, float rotation, float degreesPerSecond, + float deltaTime) { + float diff = ShortestAngleDifference(sprite->rotation, rotation); + + float maxStep = degreesPerSecond * deltaTime; + if (diff > maxStep) + diff = maxStep; + else if (diff < -maxStep) + diff = -maxStep; + + sprite->rotation += diff; +} + +void RenderSprite(const Sprite *sprite) { + DrawTexturePro(sprite->texture, sprite->sourceRec, sprite->destRec, sprite->origin, + sprite->rotation, sprite->color); } diff --git a/src/engineStorage.c b/src/engineStorage.c index 1a602a9..335ec2b 100644 --- a/src/engineStorage.c +++ b/src/engineStorage.c @@ -1,71 +1,50 @@ -#include "engine.h" +#include "engineStorage.h" -bool SaveStorageValue(const char* fileName, unsigned int position, int value){ - bool success = false; - int dataSize = 0; - unsigned int newDataSize = 0; - unsigned char *fileData = LoadFileData(fileName, &dataSize); - unsigned char *newFileData = NULL; +#include +#include +#include - if (fileData != NULL) - { - if (dataSize <= (position*sizeof(int))) - { - newDataSize = (position + 1)*sizeof(int); - newFileData = (unsigned char *)RL_REALLOC(fileData, newDataSize); - if (newFileData != NULL) - { - int *dataPtr = (int *)newFileData; - dataPtr[position]=value; - } - else - { - TraceLog(LOG_WARNING, "FLIEIO: [%s] Failed to realloc data (%u), position in bytes (%u) bigger than actual file size", fileName, dataSize, position*sizeof(int)); - newFileData = fileData; - newDataSize = dataSize; - } - } - else - { - newFileData = fileData; - newDataSize = dataSize; - int *dataPtr = (int *)newFileData; - dataPtr[position] = value; - } +bool SaveStorageData(const char *fileName, int version, const void *data, unsigned int dataSize) { + unsigned int totalSize = sizeof(int) + dataSize; + unsigned char *buffer = (unsigned char *)RL_MALLOC(totalSize); + if (buffer == NULL) + return false; - success = SaveFileData(fileName, newFileData, newDataSize); - RL_FREE(newFileData); - TraceLog(LOG_INFO, "FILEIO: [%s] Saved storage value: %i", fileName, value); - } - else - { - TraceLog(LOG_INFO, "FILEIO: [%s] File created successfully", fileName); - dataSize = (position + 1)*sizeof(int); - fileData = (unsigned char *)RL_MALLOC(dataSize); - int *dataPtr = (int *)fileData; - dataPtr[position] = value; - success = SaveFileData(fileName, fileData, dataSize); - UnloadFileData(fileData); - TraceLog(LOG_INFO, "FILEIO: [%s] Saved storage value: %i", fileName, value); - } + memcpy(buffer, &version, sizeof(int)); + memcpy(buffer + sizeof(int), data, dataSize); + + bool success = SaveFileData(fileName, buffer, totalSize); + RL_FREE(buffer); + + TraceLog(LOG_INFO, "FILEIO: [%s] Saved versioned storage data (version %i)", fileName, version); return success; } -int LoadStorageValue(const char * fileName, unsigned int position) -{ - int value = 0; - int dataSize = 0; - unsigned char *fileData = LoadFileData(fileName, &dataSize); - if (fileData != NULL) - { - if (dataSize < ((int)(position*4))) TraceLog(LOG_WARNING, "FILEIO: [%s] Failed to find storage position: %i", fileName, position); - else - { - int *dataPtr = (int *)fileData; - value = dataPtr[position]; +bool LoadStorageData(const char *fileName, int expectedVersion, void *data, unsigned int dataSize) { + int fileSize = 0; + unsigned char *fileData = LoadFileData(fileName, &fileSize); + if (fileData == NULL) + return false; + + bool success = false; + unsigned int expectedSize = sizeof(int) + dataSize; + if ((unsigned int)fileSize == expectedSize) { + int fileVersion = 0; + memcpy(&fileVersion, fileData, sizeof(int)); + if (fileVersion == expectedVersion) { + memcpy(data, fileData + sizeof(int), dataSize); + success = true; } - UnloadFileData(fileData); - TraceLog(LOG_INFO, "FILEIO: [%s] Loaded storage value: %i", fileName, value); } - return value; + + UnloadFileData(fileData); + + if (success) + TraceLog(LOG_INFO, "FILEIO: [%s] Loaded versioned storage data (version %i)", fileName, + expectedVersion); + else + TraceLog(LOG_INFO, "FILEIO: [%s] No matching versioned storage data (expected version %i)", + fileName, expectedVersion); + + return success; } diff --git a/src/engineVector.c b/src/engineVector.c index 40481ba..3c2bb2f 100644 --- a/src/engineVector.c +++ b/src/engineVector.c @@ -1,4 +1,19 @@ -#include "engine.h" +#include "engineVector.h" + +#include +#include +#include + +Vector2 GetDirectionBetweenTwoVectors(Vector2 vec1, Vector2 vec2) { + Vector2 dir = {vec2.x - vec1.x, vec2.y - vec1.y}; + return Vector2Normalize(dir); +} + +float GetDistanceBetweenTwoVectors(Vector2 vec1, Vector2 vec2) { + float dx = vec1.x - vec2.x; + float dy = vec1.y - vec2.y; + return sqrtf(dx * dx + dy * dy); +} bool IsVectorZero(Vector2 v) { float epsilon = 0.01f; @@ -6,21 +21,32 @@ bool IsVectorZero(Vector2 v) { } Vector2 GetRandomVector(const Rectangle bounds) { - return (Vector2){ - bounds.x + (float)GetRandomValue(0, 10000) / 10000.0f * bounds.width, - bounds.y + (float)GetRandomValue(0, 10000) / 10000.0f * bounds.height}; + return (Vector2){bounds.x + (float)GetRandomValue(0, 10000) / 10000.0f * bounds.width, + bounds.y + (float)GetRandomValue(0, 10000) / 10000.0f * bounds.height}; } Vector2 RotationToVector2(float rotation) { float rad = rotation * (PI / 180.0f); - Vector2 vec = {cosf(rad), sinf(rad)}; - return vec; + return (Vector2){cosf(rad), sinf(rad)}; } float Vector2ToRotation(Vector2 vec) { float rad = atan2f(vec.y, vec.x); - float deg = rad * (180.0f / PI); - return deg; + return rad * (180.0f / PI); } -float Cross2D(Vector2 a, Vector2 b) { return a.x * b.y - a.y * b.x; } +float Cross2D(Vector2 a, Vector2 b) { + return a.x * b.y - a.y * b.x; +} + +#define COLLISION_STEER_RATE 120.0f +#define COLLISION_STEER_MAX_STEP 4.0f + +void ApplyCollisionSteer(float *rotation, float side, float deltaTime) { + float delta = side * COLLISION_STEER_RATE * deltaTime; + if (delta > COLLISION_STEER_MAX_STEP) + delta = COLLISION_STEER_MAX_STEP; + else if (delta < -COLLISION_STEER_MAX_STEP) + delta = -COLLISION_STEER_MAX_STEP; + *rotation += delta; +}