#include "islands.h" #include "engineCamera.h" #include "enginePhysics.h" #include "engineRectangle.h" #include "engineVector.h" #include #include #include // 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 // 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; 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 = {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}; } Islands CreateIslands(Camera2D *camera) { Islands islands = {.count = MAX_ISLANDS}; Rectangle cameraRec = GetCameraRectangle(camera); 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 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); } }