Table of Contents
- src/n_iso_engine.c
- Functions
- Attributes
- Detailed Description
- Functions Documentation
- function _tile_covers_height
- function _smooth_neighbor_h
- function _neighbor_terrain_at
- function _neighbor_should_transition
- function _neighbor_height_at
- function _iso_preset_angle
- function _iso_object_check_occlusion
- function _iso_fopen_write
- function _iso_corner_to_screen
- function _draw_tile_warped
- function _draw_segment_underside
- function _draw_segment_sides
- function _draw_height_borders
- function _compute_tile_tint
- function _cmp_draw_entry
- Attributes Documentation
- Source code
src/n_iso_engine.c
Isometric/axonometric tile engine with height maps, terrain transitions, and A* pathfinding integration. More...
Functions
| Name | |
|---|---|
| int | _tile_covers_height(const ISO_MAP * map, int mx, int my, int z) Check if a tile has geometry covering height level z. |
| float | _smooth_neighbor_h(const ISO_MAP * map, float h_self, int nx, int ny) Helper: clamp neighbor height for smooth slope rendering. |
| int | _neighbor_terrain_at(const ISO_MAP * map, int mx, int my) Edge-aware terrain lookup for chunked rendering. |
| int | _neighbor_should_transition(const ISO_MAP * map, int mx1, int my1, int mx2, int my2) |
| int | _neighbor_height_at(const ISO_MAP * map, int mx, int my) |
| float | _iso_preset_angle(int preset) Get the angle in degrees for a projection preset. |
| int | _iso_object_check_occlusion(const ISO_MAP * map, float obj_fx, float obj_fy, float obj_fz, float obj_sx, float obj_sy, float sprite_h, float sprite_half_w, float cam_px, float cam_py, float zoom, float * out_clip_y) Check if an object is occluded by tiles drawn in front of it. |
| FILE * | _iso_fopen_write(const char * path, const char * mode) |
| void | _iso_corner_to_screen(float hw, float hh, float tl, int cx, int cy, float fh, float cam_px, float cam_py, float zoom, float * sx, float * sy) Project a tile corner to screen coordinates (canonical formula). |
| void | _draw_tile_warped(ALLEGRO_BITMAP * bmp, const float vx[4], const float vy[4], int tile_w, int tile_h, ALLEGRO_COLOR tint) Draw a tile bitmap warped to 4 pre-computed screen vertices. |
| void | _draw_segment_underside(ALLEGRO_BITMAP * bmp, float base_dx, float base_dy, float phw, float phh, float tile_draw_w, float tile_draw_h, int tile_w, int tile_h) Draw the underside face of a floating segment at 65% brightness. |
| void | _draw_segment_sides(const ISO_MAP * map, float east_x, float east_y, float south_x, float south_y, float west_x, float west_y, int mx, int my, int seg_bottom, int seg_top, float lift, float height_brightness, int wall_terrain_override) Draw cliff side faces for a single segment {seg_bottom..seg_top}. |
| void | _draw_height_borders(const ISO_MAP * map, int mx, int my, int seg_idx, int seg_bottom, int seg_top, int has_underside_face, const float vx[4], const float vy[4], float lift) Draw black border outlines on the diamond edges of an elevated tile. |
| ALLEGRO_COLOR | _compute_tile_tint(int seg_top, int max_height, float intensity, float ambient_r, float ambient_g, float ambient_b, float dyn_r, float dyn_g, float dyn_b) |
| int | _cmp_draw_entry(const void * a, const void * b) |
Attributes
| Name | |
|---|---|
| IsoPrimitiveColorFilter | g_iso_primitive_color_filter |
| IsoLineJitterCompensationFn | g_iso_line_jitter_fn |
Detailed Description
Isometric/axonometric tile engine with height maps, terrain transitions, and A* pathfinding integration.
Author: Castagnier Mickael
Version: 2.0
Date: 01/03/2026
Functions Documentation
function _tile_covers_height
static int _tile_covers_height(
const ISO_MAP * map,
int mx,
int my,
int z
)
Check if a tile has geometry covering height level z.
Returns 1 if any segment of (mx,my) has bottom <= z AND top > z, meaning the tile's wall/surface extends strictly above z. Used for occlusion: a tile drawn later in painter order covers edges of earlier tiles when its wall passes above the edge height.
Requires the occluding tile to be at least 2 height units taller (top > z + 1) so that single-step stairs (+1 per tile) do not suppress the NW/NE borders, the step only partially covers the edge, and painter order handles the overlap visually.
function _smooth_neighbor_h
static inline float _smooth_neighbor_h(
const ISO_MAP * map,
float h_self,
int nx,
int ny
)
Helper: clamp neighbor height for smooth slope rendering.
If the height difference exceeds smooth_slope_max, return self height (creating a cliff instead of a slope).
function _neighbor_terrain_at
static int _neighbor_terrain_at(
const ISO_MAP * map,
int mx,
int my
)
Edge-aware terrain lookup for chunked rendering.
Parameters:
- map the map
- mx tile X (may be just outside [0, width))
- my tile Y (may be just outside [0, height))
Return: terrain index at (mx,my), or 0 if both in-chunk and neighbor lookups fail
When (mx,my) falls just outside the chunk on a cardinal edge and the caller has wired up the neighbor_terrains_* arrays, consult them so transition masks can cross chunk seams. Out-of-range without a neighbor array falls back to the standard 0-out-of-bounds semantics. Diagonal neighbors (corners of the 8-neighborhood that fall outside the chunk's own bounds) are not covered, a fully correct diagonal fix would need 4 extra corner-cell fields.
function _neighbor_should_transition
static int _neighbor_should_transition(
const ISO_MAP * map,
int mx1,
int my1,
int mx2,
int my2
)
function _neighbor_height_at
static int _neighbor_height_at(
const ISO_MAP * map,
int mx,
int my
)
function _iso_preset_angle
static float _iso_preset_angle(
int preset
)
Get the angle in degrees for a projection preset.
Parameters:
- preset one of ISO_PROJ_CLASSIC, ISO_PROJ_ISOMETRIC, ISO_PROJ_STAGGERED, ISO_PROJ_MILITARY
Return: angle in degrees
function _iso_object_check_occlusion
static int _iso_object_check_occlusion(
const ISO_MAP * map,
float obj_fx,
float obj_fy,
float obj_fz,
float obj_sx,
float obj_sy,
float sprite_h,
float sprite_half_w,
float cam_px,
float cam_py,
float zoom,
float * out_clip_y
)
Check if an object is occluded by tiles drawn in front of it.
Parameters:
- map the map
- obj_fx object fractional map X
- obj_fy object fractional map Y
- obj_fz object height
- obj_sx object screen X (camera-transformed)
- obj_sy object screen Y (camera-transformed)
- sprite_h sprite height used for occlusion bounding
- sprite_half_w half-width of the sprite footprint (occlusion bbox)
- cam_px camera X offset in pixels
- cam_py camera Y offset in pixels
- zoom current zoom
- out_clip_y optional out-pointer: when non-NULL and a partial overlap is detected, receives the screen-Y above which the sprite should be clipped (for soft-fade rendering). May be NULL.
Return: 1 if occluded, 0 if visible
Tests tiles at higher depth (mx+my) in a forward cone to see if any elevated tile's screen area overlaps the object's screen position.
function _iso_fopen_write
static FILE * _iso_fopen_write(
const char * path,
const char * mode
)
function _iso_corner_to_screen
static inline void _iso_corner_to_screen(
float hw,
float hh,
float tl,
int cx,
int cy,
float fh,
float cam_px,
float cam_py,
float zoom,
float * sx,
float * sy
)
Project a tile corner to screen coordinates (canonical formula).
Parameters:
- hw map->proj.half_w (unzoomed)
- hh map->proj.half_h (unzoomed)
- tl map->proj.tile_lift (unzoomed)
- cx corner X in tile grid (e.g. mx+1 for the East corner)
- cy corner Y in tile grid
- fh height (float, supports smooth-height interpolation)
- cam_px integer-snapped camera pixel X: floor(cam_x * zoom)
- cam_py integer-snapped camera pixel Y: floor(cam_y * zoom)
- zoom zoom factor
- sx output screen X
- sy output screen Y
Every screen vertex in the renderer must come from this function so that two tiles sharing a corner get bit-identical screen positions regardless of which tile's draw call is computing the vertex.
The camera offset (cam_px, cam_py) must be integer-snapped pixel offsets computed as floor(cam_x * zoom), floor(cam_y * zoom). This ensures the fractional pixel position of each vertex comes only from (wx * zoom), which is constant per vertex, eliminating sub-pixel aliasing shimmer during camera movement.
function _draw_tile_warped
static void _draw_tile_warped(
ALLEGRO_BITMAP * bmp,
const float vx[4],
const float vy[4],
int tile_w,
int tile_h,
ALLEGRO_COLOR tint
)
Draw a tile bitmap warped to 4 pre-computed screen vertices.
Uses two textured triangles mapped to the warped diamond shape. Vertices must come from _iso_corner_to_screen for bit-identical shared edges between adjacent tiles.
function _draw_segment_underside
static void _draw_segment_underside(
ALLEGRO_BITMAP * bmp,
float base_dx,
float base_dy,
float phw,
float phh,
float tile_draw_w,
float tile_draw_h,
int tile_w,
int tile_h
)
Draw the underside face of a floating segment at 65% brightness.
The underside is a diamond drawn at the segment's bottom height.
function _draw_segment_sides
static void _draw_segment_sides(
const ISO_MAP * map,
float east_x,
float east_y,
float south_x,
float south_y,
float west_x,
float west_y,
int mx,
int my,
int seg_bottom,
int seg_top,
float lift,
float height_brightness,
int wall_terrain_override
)
Draw cliff side faces for a single segment {seg_bottom..seg_top}.
For ground segments (seg_bottom==0) uses neighbor-aware wall height. For floating segments (seg_bottom>0) draws full segment height.
function _draw_height_borders
static void _draw_height_borders(
const ISO_MAP * map,
int mx,
int my,
int seg_idx,
int seg_bottom,
int seg_top,
int has_underside_face,
const float vx[4],
const float vy[4],
float lift
)
Draw black border outlines on the diamond edges of an elevated tile.
Rules:
- Only draws edges where height difference > 1 or neighbor missing.
- NEVER draws the top vertex (N) where NW and NE edges meet, it is always hidden behind the tile surface itself.
- At corners E, S, W where two drawn edges meet, they join naturally.
- Occlusion is handled by painter's algorithm: tiles drawn later (closer to camera) paint over borders of earlier tiles.
Diamond vertices: N=vx[0] (top), E=vx[1] (right), S=vx[2] (bottom), W=vx[3] (left)
function _compute_tile_tint
static inline ALLEGRO_COLOR _compute_tile_tint(
int seg_top,
int max_height,
float intensity,
float ambient_r,
float ambient_g,
float ambient_b,
float dyn_r,
float dyn_g,
float dyn_b
)
function _cmp_draw_entry
static int _cmp_draw_entry(
const void * a,
const void * b
)
Attributes Documentation
variable g_iso_primitive_color_filter
static IsoPrimitiveColorFilter g_iso_primitive_color_filter = NULL;
variable g_iso_line_jitter_fn
static IsoLineJitterCompensationFn g_iso_line_jitter_fn = NULL;
Source code
/*
* Nilorea Library
* Copyright (C) 2005-2026 Castagnier Mickael
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
*/
#include <math.h>
#include <stdio.h>
#include <stdlib.h> /* realloc, free, getenv */
/* Include n_astar.h BEFORE n_iso_engine.h so the iso_map_to_astar_grid
* function is enabled via the N_ASTAR_H guard */
#include "nilorea/n_astar.h"
#include "nilorea/n_iso_engine.h"
#ifndef __windows__
#include <fcntl.h>
#include <unistd.h>
/* Open a file for writing with explicit owner-only write permission (0644)
* instead of the world-writable 0666 that fopen() requests before the umask
* is applied. On Windows the POSIX mode bits do not apply, so fopen() is used
* directly. */
static FILE* _iso_fopen_write(const char* path, const char* mode) {
int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0)
return NULL;
FILE* f = fdopen(fd, mode);
if (!f)
close(fd);
return f;
}
#else
#define _iso_fopen_write(path, mode) fopen((path), (mode))
#endif
/* Per-pass timing accumulators. Host resets these at frame start,
* iso_map_draw adds elapsed microseconds per pass per invocation. See
* the doxygen block in n_iso_engine.h for usage. */
double n_iso_pass1_us = 0.0;
double n_iso_pass2_us = 0.0;
double n_iso_pass3_us = 0.0;
double n_iso_setup_us = 0.0;
#ifdef HAVE_ALLEGRO
/* Optional primitive-color filter (sRGB-aware consumers register a
* design->linear converter here). See header docstring. NULL = identity.
* Gated on HAVE_ALLEGRO because the prototype mentions ALLEGRO_COLOR
* which doesn't exist in the no-allegro test build path. */
static IsoPrimitiveColorFilter g_iso_primitive_color_filter = NULL;
void iso_set_primitive_color_filter(IsoPrimitiveColorFilter fn) {
g_iso_primitive_color_filter = fn;
}
ALLEGRO_COLOR iso_apply_primitive_color_filter(ALLEGRO_COLOR c) {
if (g_iso_primitive_color_filter) return g_iso_primitive_color_filter(c);
return c;
}
/* Line-jitter compensation callback. See header doc. */
static IsoLineJitterCompensationFn g_iso_line_jitter_fn = NULL;
void iso_set_line_jitter_compensation(IsoLineJitterCompensationFn fn) {
g_iso_line_jitter_fn = fn;
}
void iso_apply_line_jitter_compensation(float* out_dx, float* out_dy) {
if (g_iso_line_jitter_fn) {
g_iso_line_jitter_fn(out_dx, out_dy);
} else {
if (out_dx) *out_dx = 0.0f;
if (out_dy) *out_dy = 0.0f;
}
}
#endif /* HAVE_ALLEGRO */
#ifndef NOISOENGINE
/* Height-aware ISO_MAP API (Articles 747/748/934/1269/2026)
* Core API does not require Allegro and compiles everywhere.
*/
ISO_MAP* iso_map_new(int width, int height, int num_terrains, int max_height) {
__n_assert(width > 0, return NULL);
__n_assert(height > 0, return NULL);
ISO_MAP* map = NULL;
Malloc(map, ISO_MAP, 1);
__n_assert(map, return NULL);
map->width = width;
map->height = height;
map->num_terrains = num_terrains;
map->max_height = max_height;
size_t total = (size_t)width * (size_t)height;
Malloc(map->terrain, int, total);
if (!map->terrain) {
Free(map);
return NULL;
}
Malloc(map->heightmap, int, total);
if (!map->heightmap) {
Free(map->terrain);
Free(map);
return NULL;
}
Malloc(map->ability, int, total);
if (!map->ability) {
Free(map->heightmap);
Free(map->terrain);
Free(map);
return NULL;
}
memset(map->terrain, 0, total * sizeof(int));
memset(map->heightmap, 0, total * sizeof(int));
for (size_t i = 0; i < total; i++) {
map->ability[i] = WALK;
}
/* default classic 2:1 projection for 64px wide tiles */
iso_map_set_projection(map, ISO_PROJ_CLASSIC, 64);
map->segments = NULL; /* allocated on demand by iso_map_set_segments() */
/* Overlay layer: allocated and zeroed, 0 = no overlay */
Malloc(map->overlay, int, total);
if (map->overlay) memset(map->overlay, 0, total * sizeof(int));
/* default rendering flags */
map->smooth_height = 0;
map->smooth_slope_max = 1;
map->show_grid = 0;
map->hover_mx = -1;
map->hover_my = -1;
map->height_tint_intensity = 0.0f; /* disabled by default; game sets this */
/* Ambient color defaults: full daylight (no tinting) */
map->ambient_r = 1.0f;
map->ambient_g = 1.0f;
map->ambient_b = 1.0f;
map->dynamic_light_map = NULL; /* caller-owned, set before draw */
/* Draw_order cache. dirty=1 forces a first build. */
map->cached_draw_order = NULL;
map->cached_draw_order_count = 0;
map->cached_draw_order_cap = 0;
map->draw_order_dirty = 1;
return map;
} /* iso_map_new() */
void iso_map_free(ISO_MAP** map_ptr) {
__n_assert(map_ptr, return);
__n_assert(*map_ptr, return);
ISO_MAP* map = *map_ptr;
Free(map->terrain);
Free(map->heightmap);
Free(map->ability);
Free(map->segments);
Free(map->overlay);
/* Cached draw_order */
if (map->cached_draw_order) {
free(map->cached_draw_order);
map->cached_draw_order = NULL;
}
Free(*map_ptr);
} /* iso_map_free() */
static float _iso_preset_angle(int preset) {
switch (preset) {
case ISO_PROJ_ISOMETRIC:
return 30.0f;
case ISO_PROJ_STAGGERED:
return 18.43f;
case ISO_PROJ_MILITARY:
return 45.0f;
case ISO_PROJ_CLASSIC:
default:
return 26.565f;
}
}
void iso_map_set_projection(ISO_MAP* map, int preset, float tile_width) {
__n_assert(map, return);
float hw = tile_width / 2.0f;
float angle = _iso_preset_angle(preset);
float hh = hw * tanf(angle * (float)M_PI / 180.0f);
map->proj.half_w = hw;
map->proj.half_h = hh;
map->proj.angle_deg = angle;
map->proj.target_angle = angle;
map->proj.lerp_speed = 3.0f;
map->proj.tile_lift = hh;
} /* iso_map_set_projection() */
void iso_map_set_projection_target(ISO_MAP* map, int preset) {
__n_assert(map, return);
map->proj.target_angle = _iso_preset_angle(preset);
} /* iso_map_set_projection_target() */
void iso_map_lerp_projection(ISO_MAP* map, float dt) {
__n_assert(map, return);
float diff = map->proj.target_angle - map->proj.angle_deg;
if (fabsf(diff) < 0.01f) {
map->proj.angle_deg = map->proj.target_angle;
} else {
map->proj.angle_deg += diff * map->proj.lerp_speed * dt;
}
float rad = map->proj.angle_deg * (float)M_PI / 180.0f;
map->proj.half_h = map->proj.half_w * sinf(rad) / cosf(rad);
if (map->proj.half_h < 8.0f) map->proj.half_h = 8.0f;
if (map->proj.half_h > map->proj.half_w * 2.0f) map->proj.half_h = map->proj.half_w * 2.0f;
} /* iso_map_lerp_projection() */
const char* iso_projection_name(int preset) {
switch (preset) {
case ISO_PROJ_CLASSIC:
return "Classic 2:1";
case ISO_PROJ_ISOMETRIC:
return "True Isometric";
case ISO_PROJ_STAGGERED:
return "Staggered";
case ISO_PROJ_MILITARY:
return "Military";
default:
return "Unknown";
}
} /* iso_projection_name() */
void iso_map_set_projection_custom(ISO_MAP* map, float half_w, float half_h, float tile_lift) {
__n_assert(map, return);
map->proj.half_w = half_w;
map->proj.half_h = half_h;
map->proj.tile_lift = tile_lift;
map->proj.angle_deg = atanf(half_h / half_w) * 180.0f / (float)M_PI;
map->proj.target_angle = map->proj.angle_deg;
} /* iso_map_set_projection_custom() */
int iso_map_get_terrain(const ISO_MAP* map, int mx, int my) {
__n_assert(map, return 0);
if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
return map->terrain[my * map->width + mx];
} /* iso_map_get_terrain() */
void iso_map_set_terrain(ISO_MAP* map, int mx, int my, int terrain) {
__n_assert(map, return);
if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
map->terrain[my * map->width + mx] = terrain;
} /* iso_map_set_terrain() */
int iso_map_get_height(const ISO_MAP* map, int mx, int my) {
__n_assert(map, return 0);
if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
return map->heightmap[my * map->width + mx];
} /* iso_map_get_height() */
void iso_map_set_height(ISO_MAP* map, int mx, int my, int h) {
__n_assert(map, return);
if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
if (h < 0) h = 0;
if (h > map->max_height) h = map->max_height;
map->heightmap[my * map->width + mx] = h;
} /* iso_map_set_height() */
int iso_map_get_ability(const ISO_MAP* map, int mx, int my) {
__n_assert(map, return 0);
if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
return map->ability[my * map->width + mx];
} /* iso_map_get_ability() */
void iso_map_set_ability(ISO_MAP* map, int mx, int my, int ab) {
__n_assert(map, return);
if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
map->ability[my * map->width + mx] = ab;
} /* iso_map_set_ability() */
void iso_map_to_screen(const ISO_MAP* map, int mx, int my, int h, float* screen_x, float* screen_y) {
__n_assert(map, return);
__n_assert(screen_x, return);
__n_assert(screen_y, return);
*screen_x = ((float)mx - (float)my) * map->proj.half_w + map->proj.half_w;
*screen_y = ((float)mx + (float)my) * map->proj.half_h - (float)h * map->proj.tile_lift;
} /* iso_map_to_screen() */
void iso_screen_to_map(const ISO_MAP* map, float screen_x, float screen_y, int* mx, int* my) {
__n_assert(map, return);
__n_assert(mx, return);
__n_assert(my, return);
float tw = 2.0f * map->proj.half_w;
float th = 2.0f * map->proj.half_h;
*mx = (int)floorf(screen_x / tw + screen_y / th - 1.0f);
*my = (int)floorf(screen_y / th - screen_x / tw + 1.0f);
} /* iso_screen_to_map() */
void iso_map_to_screen_f(const ISO_MAP* map, float fmx, float fmy, float h, float* screen_x, float* screen_y) {
__n_assert(map, return);
__n_assert(screen_x, return);
__n_assert(screen_y, return);
*screen_x = (fmx - fmy) * map->proj.half_w + 2.0f * map->proj.half_w;
*screen_y = (fmx + fmy) * map->proj.half_h - h * map->proj.tile_lift;
} /* iso_map_to_screen_f() */
void iso_corner_to_screen(const ISO_MAP* map, int cx, int cy, float fh, float cam_px, float cam_py, float zoom, float* sx, float* sy) {
__n_assert(map, return);
__n_assert(sx, return);
__n_assert(sy, return);
float hw = map->proj.half_w;
float hh = map->proj.half_h;
float tl = map->proj.tile_lift;
float wx = (float)(cx - cy) * hw + 2.0f * hw;
float wy = (float)(cx + cy) * hh - fh * tl;
*sx = wx * zoom + cam_px;
*sy = wy * zoom + cam_py;
} /* iso_corner_to_screen() */
int iso_is_in_diamond(int px, int py, int tile_w, int tile_h) {
float cx = (float)tile_w / 2.0f;
float cy = (float)tile_h / 2.0f;
float dx = fabsf((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
float dy = fabsf((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
return (dx + dy) <= 1.0f;
} /* iso_is_in_diamond() */
float iso_diamond_dist(int px, int py, int tile_w, int tile_h) {
float cx = (float)tile_w / 2.0f;
float cy = (float)tile_h / 2.0f;
float dx = fabsf((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
float dy = fabsf((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
float d = dx + dy;
if (d > 1.0f) return 0.0f;
return 1.0f - d;
} /* iso_diamond_dist() */
void iso_screen_to_map_height(const ISO_MAP* map, float screen_x, float screen_y, int tile_w, int tile_h, int* mx, int* my) {
__n_assert(map, return);
__n_assert(mx, return);
__n_assert(my, return);
float half_w = map->proj.half_w;
float half_h = map->proj.half_h;
float tile_lift = map->proj.tile_lift;
float tw = half_w * 2.0f;
float th = half_h * 2.0f;
for (int h = map->max_height; h >= 0; h--) {
float adj_sy = screen_y + (float)h * tile_lift;
float fmx = screen_x / tw + adj_sy / th - 1.0f;
float fmy = adj_sy / th - screen_x / tw + 1.0f;
int tmx = (int)floorf(fmx);
int tmy = (int)floorf(fmy);
if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height)
continue;
if (iso_map_get_height(map, tmx, tmy) == h) {
float tile_sx, tile_sy;
iso_map_to_screen(map, tmx, tmy, h, &tile_sx, &tile_sy);
float norm_x = (screen_x - tile_sx) / tw * (float)tile_w;
float norm_y = (screen_y - tile_sy) / th * (float)tile_h;
int lpx = (int)norm_x;
int lpy = (int)norm_y;
if (lpx >= 0 && lpx < tile_w && lpy >= 0 && lpy < tile_h &&
iso_is_in_diamond(lpx, lpy, tile_w, tile_h)) {
*mx = tmx;
*my = tmy;
return;
}
}
}
/* Fallback: flat projection (height 0) */
*mx = (int)floorf(screen_x / tw + screen_y / th - 1.0f);
*my = (int)floorf(screen_y / th - screen_x / tw + 1.0f);
} /* iso_screen_to_map_height() */
void iso_screen_to_map_height_f(const ISO_MAP* map, float screen_x, float screen_y, int tile_w, int tile_h, int* mx, int* my, float* out_fx, float* out_fy) {
__n_assert(map, return);
__n_assert(mx, return);
__n_assert(my, return);
float half_w = map->proj.half_w;
float half_h = map->proj.half_h;
float tile_lift = map->proj.tile_lift;
float tw = half_w * 2.0f;
float th = half_h * 2.0f;
for (int h = map->max_height; h >= 0; h--) {
float adj_sy = screen_y + (float)h * tile_lift;
float fmx = screen_x / tw + adj_sy / th - 1.0f;
float fmy = adj_sy / th - screen_x / tw + 1.0f;
int tmx = (int)floorf(fmx);
int tmy = (int)floorf(fmy);
if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height)
continue;
if (iso_map_get_height(map, tmx, tmy) == h) {
float tile_sx, tile_sy;
iso_map_to_screen(map, tmx, tmy, h, &tile_sx, &tile_sy);
float norm_x = (screen_x - tile_sx) / tw * (float)tile_w;
float norm_y = (screen_y - tile_sy) / th * (float)tile_h;
int lpx = (int)norm_x;
int lpy = (int)norm_y;
if (lpx >= 0 && lpx < tile_w && lpy >= 0 && lpy < tile_h &&
iso_is_in_diamond(lpx, lpy, tile_w, tile_h)) {
*mx = tmx;
*my = tmy;
if (out_fx || out_fy) {
/* Refine fractional coordinates using interpolated height
* so that iso_map_to_screen_f(fx, fy, interp_h) returns
* the original screen position. Without this, sub-tile
* positions are off when the bilinear-interpolated height
* differs from the tile's integer height (e.g. at edges
* between tiles of different heights).
*
* Only refine in SMOOTH mode. In CUT mode the rendered
* tile sits at the flat integer height, so the initial
* fmx/fmy (computed at that height) are already correct.
* Refining with bilinear-interpolated heights in CUT mode
* pulls the position toward tile corners and causes
* click-to-move to land at the wrong spot. */
float refined_fx = fmx;
float refined_fy = fmy;
if (map->smooth_height) {
/* Upper bounds: allow the full extent of the last tile.
* Tile tmx occupies [tmx, tmx+1) in map coords, and the
* last valid position must stay < map->width so that
* floor() never yields an out-of-bounds tile index. */
float map_max_fx = (float)map->width - 1e-4f;
float map_max_fy = (float)map->height - 1e-4f;
/* Tile-local bounds to prevent divergence: clamp each
* refinement step to the detected tile so that large
* height differences with neighbours cannot pull the
* position into an adjacent tile. */
float tile_min_fx = (float)tmx;
float tile_min_fy = (float)tmy;
float tile_max_fx = (float)(tmx + 1);
float tile_max_fy = (float)(tmy + 1);
if (tile_max_fx > map_max_fx) tile_max_fx = map_max_fx;
if (tile_max_fy > map_max_fy) tile_max_fy = map_max_fy;
for (int iter = 0; iter < 3; iter++) {
float interp_h = iso_map_interpolate_height(map, refined_fx, refined_fy);
float new_adj_sy = screen_y + interp_h * tile_lift;
refined_fx = screen_x / tw + new_adj_sy / th - 1.0f;
refined_fy = new_adj_sy / th - screen_x / tw + 1.0f;
/* Keep within detected tile */
if (refined_fx < tile_min_fx) refined_fx = tile_min_fx;
if (refined_fy < tile_min_fy) refined_fy = tile_min_fy;
if (refined_fx > tile_max_fx) refined_fx = tile_max_fx;
if (refined_fy > tile_max_fy) refined_fy = tile_max_fy;
}
}
/* Clamp to map bounds so border clicks stay valid */
float map_max_fx2 = (float)map->width - 1e-4f;
float map_max_fy2 = (float)map->height - 1e-4f;
if (refined_fx < 0.0f) refined_fx = 0.0f;
if (refined_fy < 0.0f) refined_fy = 0.0f;
if (refined_fx > map_max_fx2) refined_fx = map_max_fx2;
if (refined_fy > map_max_fy2) refined_fy = map_max_fy2;
if (out_fx) *out_fx = refined_fx;
if (out_fy) *out_fy = refined_fy;
}
return;
}
}
}
/* Fallback: flat projection (height 0) */
float fmx = screen_x / tw + screen_y / th - 1.0f;
float fmy = screen_y / th - screen_x / tw + 1.0f;
*mx = (int)floorf(fmx);
*my = (int)floorf(fmy);
if (out_fx) *out_fx = fmx;
if (out_fy) *out_fy = fmy;
} /* iso_screen_to_map_height_f() */
float iso_map_interpolate_height(const ISO_MAP* map, float fx, float fy) {
__n_assert(map, return 0.0f);
/* Clamp to valid map range so border tiles interpolate correctly.
* Allow the full extent of the last tile (up to width/height minus
* a tiny epsilon) so that positions in the right half of border
* tiles are not snapped to the tile corner. */
if (fx < 0.0f) fx = 0.0f;
if (fy < 0.0f) fy = 0.0f;
float max_fx = (float)map->width - 1e-4f;
float max_fy = (float)map->height - 1e-4f;
if (fx > max_fx) fx = max_fx;
if (fy > max_fy) fy = max_fy;
int ix = (int)floorf(fx);
int iy = (int)floorf(fy);
float frac_x = fx - (float)ix;
float frac_y = fy - (float)iy;
/* Clamp neighbor indices for edge tiles */
int ix1 = (ix + 1 < map->width) ? ix + 1 : ix;
int iy1 = (iy + 1 < map->height) ? iy + 1 : iy;
float h00 = (float)iso_map_get_height(map, ix, iy);
float h10 = (float)iso_map_get_height(map, ix1, iy);
float h01 = (float)iso_map_get_height(map, ix, iy1);
float h11 = (float)iso_map_get_height(map, ix1, iy1);
float top = h00 + (h10 - h00) * frac_x;
float bot = h01 + (h11 - h01) * frac_x;
return top + (bot - top) * frac_y;
} /* iso_map_interpolate_height() */
void iso_map_calc_transitions(const ISO_MAP* map, int mx, int my, int* edge_bits, int* corner_bits) {
__n_assert(map, return);
__n_assert(edge_bits, return);
__n_assert(corner_bits, return);
int center = iso_map_get_terrain(map, mx, my);
*edge_bits = 0;
*corner_bits = 0;
/* Edge neighbors: W(-1,0), N(0,-1), E(+1,0), S(0,+1) */
int w = iso_map_get_terrain(map, mx - 1, my);
int n = iso_map_get_terrain(map, mx, my - 1);
int e = iso_map_get_terrain(map, mx + 1, my);
int s = iso_map_get_terrain(map, mx, my + 1);
if (w != center) *edge_bits |= ISO_EDGE_W;
if (n != center) *edge_bits |= ISO_EDGE_N;
if (e != center) *edge_bits |= ISO_EDGE_E;
if (s != center) *edge_bits |= ISO_EDGE_S;
/* Corner neighbors: only set if adjacent edges don't already have that terrain */
int nw = iso_map_get_terrain(map, mx - 1, my - 1);
int ne = iso_map_get_terrain(map, mx + 1, my - 1);
int se = iso_map_get_terrain(map, mx + 1, my + 1);
int sw = iso_map_get_terrain(map, mx - 1, my + 1);
if (nw != center && nw != w && nw != n) *corner_bits |= ISO_CORNER_NW;
if (ne != center && ne != n && ne != e) *corner_bits |= ISO_CORNER_NE;
if (se != center && se != e && se != s) *corner_bits |= ISO_CORNER_SE;
if (sw != center && sw != s && sw != w) *corner_bits |= ISO_CORNER_SW;
} /* iso_map_calc_transitions() */
int iso_map_should_transition(const ISO_MAP* map, int mx1, int my1, int mx2, int my2) {
__n_assert(map, return 0);
return iso_map_get_terrain(map, mx1, my1) != iso_map_get_terrain(map, mx2, my2);
} /* iso_map_should_transition() */
void iso_map_corner_heights(const ISO_MAP* map, int mx, int my, float* h_n, float* h_e, float* h_s, float* h_w) {
__n_assert(map, return);
__n_assert(h_n, return);
__n_assert(h_e, return);
__n_assert(h_s, return);
__n_assert(h_w, return);
float c = (float)iso_map_get_height(map, mx, my);
float nw = (float)iso_map_get_height(map, mx - 1, my - 1);
float nn = (float)iso_map_get_height(map, mx, my - 1);
float ne = (float)iso_map_get_height(map, mx + 1, my - 1);
float ee = (float)iso_map_get_height(map, mx + 1, my);
float se = (float)iso_map_get_height(map, mx + 1, my + 1);
float ss = (float)iso_map_get_height(map, mx, my + 1);
float sw = (float)iso_map_get_height(map, mx - 1, my + 1);
float ww = (float)iso_map_get_height(map, mx - 1, my);
*h_n = (c + nw + nn + ww) / 4.0f;
*h_e = (c + nn + ne + ee) / 4.0f;
*h_s = (c + ee + se + ss) / 4.0f;
*h_w = (c + ss + sw + ww) / 4.0f;
} /* iso_map_corner_heights() */
int iso_map_save(const ISO_MAP* map, const char* filename) {
__n_assert(map, return FALSE);
__n_assert(filename, return FALSE);
FILE* f = _iso_fopen_write(filename, "wb");
if (!f) {
n_log(LOG_ERR, "iso_map_save: could not open %s for writing", filename);
return FALSE;
}
/* magic */
fwrite("ISOM", 1, 4, f);
/* header */
const int32_t hdr[4] = {map->width, map->height, map->num_terrains, map->max_height};
fwrite(hdr, sizeof(int32_t), 4, f);
size_t total = (size_t)map->width * (size_t)map->height;
/* terrain layer */
fwrite(map->terrain, sizeof(int), total, f);
/* heightmap layer */
fwrite(map->heightmap, sizeof(int), total, f);
/* ability layer */
fwrite(map->ability, sizeof(int), total, f);
fclose(f);
return TRUE;
} /* iso_map_save() */
ISO_MAP* iso_map_load(const char* filename) {
__n_assert(filename, return NULL);
FILE* f = fopen(filename, "rb");
if (!f) {
n_log(LOG_ERR, "iso_map_load: could not open %s for reading", filename);
return NULL;
}
/* check magic */
char magic[4];
if (fread(magic, 1, 4, f) != 4 ||
magic[0] != 'I' || magic[1] != 'S' || magic[2] != 'O' || magic[3] != 'M') {
n_log(LOG_ERR, "iso_map_load: invalid file format");
fclose(f);
return NULL;
}
int32_t hdr[4];
if (fread(hdr, sizeof(int32_t), 4, f) != 4) {
n_log(LOG_ERR, "iso_map_load: truncated header");
fclose(f);
return NULL;
}
ISO_MAP* map = iso_map_new(hdr[0], hdr[1], hdr[2], hdr[3]);
if (!map) {
fclose(f);
return NULL;
}
/* each read only runs after the previous one returned everything it
* asked for: a short read leaves the stream position indeterminate */
size_t total = (size_t)map->width * (size_t)map->height;
if (fread(map->terrain, sizeof(int), total, f) != total ||
fread(map->heightmap, sizeof(int), total, f) != total ||
fread(map->ability, sizeof(int), total, f) != total) {
n_log(LOG_ERR, "iso_map_load: truncated data");
iso_map_free(&map);
fclose(f);
return NULL;
}
fclose(f);
return map;
} /* iso_map_load() */
void iso_map_randomize(ISO_MAP* map) {
__n_assert(map, return);
for (int y = 0; y < map->height; y++) {
for (int x = 0; x < map->width; x++) {
int idx = y * map->width + x;
map->terrain[idx] = rand() % map->num_terrains;
map->heightmap[idx] = rand() % (map->max_height + 1);
map->ability[idx] = WALK;
}
}
} /* iso_map_randomize() */
static inline float _smooth_neighbor_h(const ISO_MAP* map, float h_self, int nx, int ny) {
float nh = (float)iso_map_get_height(map, nx, ny);
if (fabsf(h_self - nh) > (float)map->smooth_slope_max) return h_self;
return nh;
}
void iso_map_smooth_corner_heights(const ISO_MAP* map, int mx, int my, float* h_n, float* h_e, float* h_s, float* h_w) {
__n_assert(map, return);
__n_assert(h_n, return);
__n_assert(h_e, return);
__n_assert(h_s, return);
__n_assert(h_w, return);
float h = (float)iso_map_get_height(map, mx, my);
*h_n = (h + _smooth_neighbor_h(map, h, mx - 1, my) + _smooth_neighbor_h(map, h, mx, my - 1) + _smooth_neighbor_h(map, h, mx - 1, my - 1)) / 4.0f;
*h_e = (h + _smooth_neighbor_h(map, h, mx, my - 1) + _smooth_neighbor_h(map, h, mx + 1, my) + _smooth_neighbor_h(map, h, mx + 1, my - 1)) / 4.0f;
*h_s = (h + _smooth_neighbor_h(map, h, mx + 1, my) + _smooth_neighbor_h(map, h, mx, my + 1) + _smooth_neighbor_h(map, h, mx + 1, my + 1)) / 4.0f;
*h_w = (h + _smooth_neighbor_h(map, h, mx - 1, my) + _smooth_neighbor_h(map, h, mx, my + 1) + _smooth_neighbor_h(map, h, mx - 1, my + 1)) / 4.0f;
} /* iso_map_smooth_corner_heights() */
/* Per-tile segment API */
int iso_map_set_segments(ISO_MAP* map, int mx, int my, const ISO_TILE_SEGMENT* segs, int count) {
__n_assert(map, return 0);
if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
if (count < 0 || count > ISO_MAX_SEGMENTS_PER_TILE) return 0;
/* Validate invariants */
for (int i = 0; i < count; i++) {
if (segs[i].bottom >= segs[i].top) return 0;
if (i > 0 && segs[i - 1].top > segs[i].bottom) return 0;
}
/* Allocate segment array on first use */
if (!map->segments) {
size_t total = (size_t)map->width * (size_t)map->height;
Malloc(map->segments, ISO_TILE_SEGMENTS, total);
if (!map->segments) return 0;
memset(map->segments, 0, total * sizeof(ISO_TILE_SEGMENTS));
}
int idx = my * map->width + mx;
map->segments[idx].count = count;
for (int i = 0; i < count; i++) map->segments[idx].segs[i] = segs[i];
for (int i = count; i < ISO_MAX_SEGMENTS_PER_TILE; i++) {
map->segments[idx].segs[i].bottom = 0;
map->segments[idx].segs[i].top = 0;
map->segments[idx].segs[i].upper_tile = -1;
map->segments[idx].segs[i].lower_tile = -1;
}
/* Update derived heightmap */
map->heightmap[idx] = (count > 0) ? segs[count - 1].top : 0;
/* Draw_order depends on segments + heights; invalidate. */
map->draw_order_dirty = 1;
return 1;
}
const ISO_TILE_SEGMENTS* iso_map_get_segments(const ISO_MAP* map, int mx, int my) {
if (!map || !map->segments) return NULL;
if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return NULL;
return &map->segments[my * map->width + mx];
}
static int _cmp_draw_entry(const void* a, const void* b) {
const ISO_DRAW_ENTRY* ea = (const ISO_DRAW_ENTRY*)a;
const ISO_DRAW_ENTRY* eb = (const ISO_DRAW_ENTRY*)b;
/* Primary: isometric depth (back-to-front) */
int depth_a = ea->my + ea->mx;
int depth_b = eb->my + eb->mx;
if (depth_a != depth_b) return depth_a - depth_b;
/* Secondary: segment base height (lower drawn first) */
if (ea->bottom != eb->bottom) return ea->bottom - eb->bottom;
/* Tertiary: row then column */
if (ea->my != eb->my) return ea->my - eb->my;
return ea->mx - eb->mx;
}
int iso_map_build_draw_order(const ISO_MAP* map, ISO_DRAW_ENTRY* out, int max_entries) {
__n_assert(map, return 0);
__n_assert(out, return 0);
int n = 0;
for (int my = 0; my < map->height; my++) {
for (int mx = 0; mx < map->width; mx++) {
int idx = my * map->width + mx;
if (map->segments && map->segments[idx].count > 0) {
const ISO_TILE_SEGMENTS* ts = &map->segments[idx];
for (int si = 0; si < ts->count && n < max_entries; si++) {
out[n].mx = mx;
out[n].my = my;
out[n].seg_idx = si;
out[n].bottom = ts->segs[si].bottom;
out[n].top = ts->segs[si].top;
/* underside: bottom > 0 and no segment directly below */
out[n].underside = (ts->segs[si].bottom > 0 &&
(si == 0 || ts->segs[si - 1].top < ts->segs[si].bottom));
n++;
}
} else {
/* No segments or count==0: single entry from heightmap */
if (n >= max_entries) continue;
int h = map->heightmap[idx];
out[n].mx = mx;
out[n].my = my;
out[n].seg_idx = 0;
out[n].bottom = 0;
out[n].top = h;
out[n].underside = 0;
n++;
}
}
}
qsort(out, (size_t)n, sizeof(ISO_DRAW_ENTRY), _cmp_draw_entry);
return n;
}
int iso_map_should_transition_smooth(const ISO_MAP* map, int mx1, int my1, int mx2, int my2) {
__n_assert(map, return 0);
int h1 = iso_map_get_height(map, mx1, my1);
int h2 = iso_map_get_height(map, mx2, my2);
if (map->smooth_height)
return abs(h1 - h2) <= map->smooth_slope_max;
return h1 == h2;
} /* iso_map_should_transition_smooth() */
static int _neighbor_terrain_at(const ISO_MAP* map, int mx, int my) {
if (my >= 0 && my < map->height) {
if (mx == -1 && map->neighbor_terrains_west)
return map->neighbor_terrains_west[my];
if (mx == map->width && map->neighbor_terrains_east)
return map->neighbor_terrains_east[my];
}
if (mx >= 0 && mx < map->width) {
if (my == -1 && map->neighbor_terrains_north)
return map->neighbor_terrains_north[mx];
if (my == map->height && map->neighbor_terrains_south)
return map->neighbor_terrains_south[mx];
}
return iso_map_get_terrain(map, mx, my);
}
static int _neighbor_height_at(const ISO_MAP* map, int mx, int my) {
if (my >= 0 && my < map->height) {
if (mx == -1 && map->neighbor_heights_west)
return map->neighbor_heights_west[my];
if (mx == map->width && map->neighbor_heights_east)
return map->neighbor_heights_east[my];
}
if (mx >= 0 && mx < map->width) {
if (my == -1 && map->neighbor_heights_north)
return map->neighbor_heights_north[mx];
if (my == map->height && map->neighbor_heights_south)
return map->neighbor_heights_south[mx];
}
return iso_map_get_height(map, mx, my);
}
static int _neighbor_should_transition(const ISO_MAP* map, int mx1, int my1, int mx2, int my2) {
int h1 = _neighbor_height_at(map, mx1, my1);
int h2 = _neighbor_height_at(map, mx2, my2);
if (map->smooth_height)
return abs(h1 - h2) <= map->smooth_slope_max;
return h1 == h2;
}
void iso_map_calc_transitions_full(const ISO_MAP* map, int mx, int my, int* edge_bits, int* corner_bits) {
__n_assert(map, return);
__n_assert(edge_bits, return);
__n_assert(corner_bits, return);
int base = iso_map_get_terrain(map, mx, my);
memset(edge_bits, 0, sizeof(int) * (size_t)map->num_terrains);
memset(corner_bits, 0, sizeof(int) * (size_t)map->num_terrains);
/* Cardinal neighbors: only if height-connected. Uses the
* _neighbor_* helpers so chunk-edge lookups consult the
* neighbor_terrains_* / neighbor_heights_* arrays instead of
* falling to the default 0-out-of-bounds and dropping the
* cross-chunk transition. */
int t_w = _neighbor_should_transition(map, mx, my, mx - 1, my)
? _neighbor_terrain_at(map, mx - 1, my)
: base;
int t_n = _neighbor_should_transition(map, mx, my, mx, my - 1)
? _neighbor_terrain_at(map, mx, my - 1)
: base;
int t_e = _neighbor_should_transition(map, mx, my, mx + 1, my)
? _neighbor_terrain_at(map, mx + 1, my)
: base;
int t_s = _neighbor_should_transition(map, mx, my, mx, my + 1)
? _neighbor_terrain_at(map, mx, my + 1)
: base;
/* Diagonal neighbors, see _neighbor_terrain_at comment. Diagonals
* that fall off the chunk still return 0 (water); the visible
* fallout is single-tile corner artifacts at chunk boundaries
* rather than whole-edge seams. */
int t_nw = _neighbor_should_transition(map, mx, my, mx - 1, my - 1)
? _neighbor_terrain_at(map, mx - 1, my - 1)
: base;
int t_ne = _neighbor_should_transition(map, mx, my, mx + 1, my - 1)
? _neighbor_terrain_at(map, mx + 1, my - 1)
: base;
int t_se = _neighbor_should_transition(map, mx, my, mx + 1, my + 1)
? _neighbor_terrain_at(map, mx + 1, my + 1)
: base;
int t_sw = _neighbor_should_transition(map, mx, my, mx - 1, my + 1)
? _neighbor_terrain_at(map, mx - 1, my + 1)
: base;
for (int t = base + 1; t < map->num_terrains; t++) {
if (t_w == t) edge_bits[t] |= ISO_EDGE_W;
if (t_n == t) edge_bits[t] |= ISO_EDGE_N;
if (t_e == t) edge_bits[t] |= ISO_EDGE_E;
if (t_s == t) edge_bits[t] |= ISO_EDGE_S;
if (t_nw == t && !(edge_bits[t] & ISO_EDGE_W) && !(edge_bits[t] & ISO_EDGE_N))
corner_bits[t] |= ISO_CORNER_NW;
if (t_ne == t && !(edge_bits[t] & ISO_EDGE_N) && !(edge_bits[t] & ISO_EDGE_E))
corner_bits[t] |= ISO_CORNER_NE;
if (t_se == t && !(edge_bits[t] & ISO_EDGE_E) && !(edge_bits[t] & ISO_EDGE_S))
corner_bits[t] |= ISO_CORNER_SE;
if (t_sw == t && !(edge_bits[t] & ISO_EDGE_S) && !(edge_bits[t] & ISO_EDGE_W))
corner_bits[t] |= ISO_CORNER_SW;
}
} /* iso_map_calc_transitions_full() */
/* N_ISO_CAMERA */
N_ISO_CAMERA* n_iso_camera_new(float zoom_min, float zoom_max) {
N_ISO_CAMERA* cam = NULL;
Malloc(cam, N_ISO_CAMERA, 1);
__n_assert(cam, return NULL);
cam->x = 0.0f;
cam->y = 0.0f;
cam->zoom = 1.0f;
cam->zoom_min = zoom_min;
cam->zoom_max = zoom_max;
return cam;
} /* n_iso_camera_new() */
void n_iso_camera_free(N_ISO_CAMERA** cam) {
__n_assert(cam && *cam, return);
Free(*cam);
} /* n_iso_camera_free() */
void n_iso_camera_scroll(N_ISO_CAMERA* cam, float dx, float dy) {
__n_assert(cam, return);
cam->x += dx;
cam->y += dy;
} /* n_iso_camera_scroll() */
void n_iso_camera_zoom(N_ISO_CAMERA* cam, float dz, float mouse_x, float mouse_y) {
__n_assert(cam, return);
float old_zoom = cam->zoom;
cam->zoom += dz;
if (cam->zoom < cam->zoom_min) cam->zoom = cam->zoom_min;
if (cam->zoom > cam->zoom_max) cam->zoom = cam->zoom_max;
/* Adjust position so the world point under the mouse stays fixed */
cam->x += mouse_x / cam->zoom - mouse_x / old_zoom;
cam->y += mouse_y / cam->zoom - mouse_y / old_zoom;
} /* n_iso_camera_zoom() */
void n_iso_camera_center_on(N_ISO_CAMERA* cam, float world_x, float world_y, int screen_w, int screen_h) {
__n_assert(cam, return);
cam->x = -world_x + (float)screen_w / (2.0f * cam->zoom);
cam->y = -world_y + (float)screen_h / (2.0f * cam->zoom);
} /* n_iso_camera_center_on() */
void n_iso_camera_follow(N_ISO_CAMERA* cam, float target_x, float target_y, int screen_w, int screen_h, float smoothing, float dt) {
__n_assert(cam, return);
float target_cx = -target_x + (float)screen_w / (2.0f * cam->zoom);
float target_cy = -target_y + (float)screen_h / (2.0f * cam->zoom);
float t = smoothing * dt;
if (t > 1.0f) t = 1.0f;
cam->x += (target_cx - cam->x) * t;
cam->y += (target_cy - cam->y) * t;
} /* n_iso_camera_follow() */
void n_iso_camera_screen_to_world(const N_ISO_CAMERA* cam, float sx, float sy, float* wx, float* wy) {
__n_assert(cam, return);
__n_assert(wx, return);
__n_assert(wy, return);
*wx = sx / cam->zoom - cam->x;
*wy = sy / cam->zoom - cam->y;
} /* n_iso_camera_screen_to_world() */
void n_iso_camera_world_to_screen(const N_ISO_CAMERA* cam, float wx, float wy, float* sx, float* sy) {
__n_assert(cam, return);
__n_assert(sx, return);
__n_assert(sy, return);
*sx = (wx + cam->x) * cam->zoom;
*sy = (wy + cam->y) * cam->zoom;
} /* n_iso_camera_world_to_screen() */
/* Height border edge visibility (public API, no Allegro dependency) */
ISO_VISIBLE_EDGES iso_map_get_visible_edges(const ISO_MAP* map, int mx, int my) {
ISO_VISIBLE_EDGES edges = {0};
if (!map) return edges;
int h = iso_map_get_height(map, mx, my);
if (h == 0) return edges;
/* NW edge: neighbor (mx-1, my) */
if (mx <= 0) {
/* At west boundary: use neighbor chunk data if available */
if (map->neighbor_heights_west) {
if (abs(h - map->neighbor_heights_west[my]) >= 1)
edges.draw_nw = 1;
} else {
edges.draw_nw = 1; /* world boundary */
}
} else if (abs(h - iso_map_get_height(map, mx - 1, my)) >= 1) {
edges.draw_nw = 1;
}
/* NE edge: neighbor (mx, my-1) */
if (my <= 0) {
if (map->neighbor_heights_north) {
if (abs(h - map->neighbor_heights_north[mx]) >= 1)
edges.draw_ne = 1;
} else {
edges.draw_ne = 1;
}
} else if (abs(h - iso_map_get_height(map, mx, my - 1)) >= 1) {
edges.draw_ne = 1;
}
/* SE edge: neighbor (mx+1, my) */
if (mx >= map->width - 1) {
if (map->neighbor_heights_east) {
if (abs(h - map->neighbor_heights_east[my]) >= 1)
edges.draw_se = 1;
} else {
edges.draw_se = 1;
}
} else if (abs(h - iso_map_get_height(map, mx + 1, my)) >= 1) {
edges.draw_se = 1;
}
/* SW edge: neighbor (mx, my+1) */
if (my >= map->height - 1) {
if (map->neighbor_heights_south) {
if (abs(h - map->neighbor_heights_south[mx]) >= 1)
edges.draw_sw = 1;
} else {
edges.draw_sw = 1;
}
} else if (abs(h - iso_map_get_height(map, mx, my + 1)) >= 1) {
edges.draw_sw = 1;
}
return edges;
}
static int _tile_covers_height(const ISO_MAP* map, int mx, int my, int z) {
if (!map || mx < 0 || mx >= map->width || my < 0 || my >= map->height)
return 0;
const ISO_TILE_SEGMENTS* ts = iso_map_get_segments(map, mx, my);
if (ts && ts->count > 0) {
for (int i = 0; i < ts->count; i++) {
if (ts->segs[i].bottom <= z && ts->segs[i].top > z + 1)
return 1;
}
return 0;
}
int h = iso_map_get_height(map, mx, my);
return (h > z + 1) ? 1 : 0;
}
ISO_VISIBLE_EDGES iso_map_get_visible_edges_segment(const ISO_MAP* map, int mx, int my, int seg_idx) {
ISO_VISIBLE_EDGES edges = {0};
if (!map) return edges;
/* Get this tile's segment */
ISO_TILE_SEGMENT seg;
const ISO_TILE_SEGMENTS* ts = iso_map_get_segments(map, mx, my);
if (ts && seg_idx >= 0 && seg_idx < ts->count) {
seg = ts->segs[seg_idx];
} else {
/* Fallback: heightmap as single segment {0, h} */
int h = iso_map_get_height(map, mx, my);
if (h == 0) return edges;
seg.bottom = 0;
seg.top = h;
}
if (seg.top <= seg.bottom) return edges;
/* Direction offsets: NW->(mx-1,my), NE->(mx,my-1), SE->(mx+1,my), SW->(mx,my+1) */
static const int ddx[4] = {-1, 0, 1, 0};
static const int ddy[4] = {0, -1, 0, 1};
int exposed[4] = {0, 0, 0, 0}; /* edge to draw, per direction */
int any_side_exposed = 0; /* for underside check */
int ntop[4] = {0, 0, 0, 0}; /* neighbor top height per direction */
for (int d = 0; d < 4; d++) {
int nx = mx + ddx[d];
int ny = my + ddy[d];
int neighbor_matches = 0;
int neighbor_covers_bottom = 0;
if (nx < 0 || nx >= map->width || ny < 0 || ny >= map->height) {
/* Boundary: check neighbor chunk height arrays */
int nh = -1;
if (d == 0 && map->neighbor_heights_west) nh = map->neighbor_heights_west[my];
if (d == 1 && map->neighbor_heights_north) nh = map->neighbor_heights_north[mx];
if (d == 2 && map->neighbor_heights_east) nh = map->neighbor_heights_east[my];
if (d == 3 && map->neighbor_heights_south) nh = map->neighbor_heights_south[mx];
if (nh > 0) {
/* Boundary treated as single segment {0,nh}: match if same top */
neighbor_matches = (nh == seg.top);
neighbor_covers_bottom = (nh >= seg.bottom);
ntop[d] = nh;
}
/* else: world boundary -> no match, not covered, ntop stays 0 */
} else {
const ISO_TILE_SEGMENTS* nts = iso_map_get_segments(map, nx, ny);
if (nts && nts->count > 0) {
/* Match if any neighbour segment has the same top height.
* This applies uniformly to ground and floating segments:
* same top = connected surface = no border.
* Different top = visible height step = border drawn. */
for (int si = 0; si < nts->count; si++) {
if (nts->segs[si].top == seg.top) {
neighbor_matches = 1;
}
if (nts->segs[si].top >= seg.bottom) {
neighbor_covers_bottom = 1;
}
}
/* Use heightmap value as neighbor top for vertical lines */
ntop[d] = iso_map_get_height(map, nx, ny);
} else {
/* No segment data: heightmap as single segment {0, h} */
int nh = iso_map_get_height(map, nx, ny);
if (nh > 0) {
neighbor_matches = (nh == seg.top);
neighbor_covers_bottom = (nh >= seg.bottom);
}
ntop[d] = nh;
}
}
if (!neighbor_matches) exposed[d] = 1;
if (!neighbor_covers_bottom) any_side_exposed = 1;
}
edges.draw_nw = exposed[0];
edges.draw_ne = exposed[1];
edges.draw_se = exposed[2];
edges.draw_sw = exposed[3];
/* OCCLUSION: NW/NE edges face away from camera.
* Suppress if the tile drawn later (south in painter order) has
* geometry covering this segment's top face height.
* NW edge: covered by SW neighbour (mx, my+1)
* NE edge: covered by SE neighbour (mx+1, my) */
if (edges.draw_nw && _tile_covers_height(map, mx, my + 1, seg.top))
edges.draw_nw = 0;
if (edges.draw_ne && _tile_covers_height(map, mx + 1, my, seg.top))
edges.draw_ne = 0;
/* Underside border for floating segments */
if (seg.bottom > 0 && any_side_exposed) {
edges.draw_underside = 1;
}
/* Store neighbor top heights for vertical corner line computation */
edges.neighbor_top_nw = ntop[0];
edges.neighbor_top_ne = ntop[1];
edges.neighbor_top_se = ntop[2];
edges.neighbor_top_sw = ntop[3];
return edges;
}
/* ISO_MAP rendering (requires Allegro 5) */
#ifdef HAVE_ALLEGRO
#include <allegro5/allegro.h>
#include <allegro5/allegro_primitives.h>
static inline void _iso_corner_to_screen(float hw, float hh, float tl, int cx, int cy, float fh, float cam_px, float cam_py, float zoom, float* sx, float* sy) {
float wx = (float)(cx - cy) * hw + 2.0f * hw;
float wy = (float)(cx + cy) * hh - fh * tl;
*sx = wx * zoom + cam_px;
*sy = wy * zoom + cam_py;
}
/* Extended tint: combines height tint with ambient color and per-tile dynamic light.
* ambient_r/g/b: global day/night ambient (1.0 = full daylight).
* dyn_r/g/b: additive dynamic light contribution at this tile (0.0 = no light). */
static inline ALLEGRO_COLOR _compute_tile_tint(int seg_top, int max_height, float intensity, float ambient_r, float ambient_g, float ambient_b, float dyn_r, float dyn_g, float dyn_b) {
/* Start with height-based brightness */
float hb = 1.0f;
if (intensity > 0.0f && max_height > 0) {
float ratio = (float)seg_top / (float)max_height;
hb = 1.0f - intensity * (1.0f - ratio);
if (hb < 0.0f) hb = 0.0f;
if (hb > 1.0f) hb = 1.0f;
}
/* Multiply by ambient color */
float r = hb * ambient_r;
float g = hb * ambient_g;
float b = hb * ambient_b;
/* Add dynamic light (additive boost) */
r += dyn_r;
g += dyn_g;
b += dyn_b;
/* Clamp to avoid overexposure */
if (r > 1.0f) r = 1.0f;
if (g > 1.0f) g = 1.0f;
if (b > 1.0f) b = 1.0f;
/* The tint multiplier is sRGB-designed (ambient curve + dyn-light
* literals tuned on a non-sRGB display). Route through the primitive
* color filter so an sRGB-aware consumer can linearise it before it
* multiplies the linear-decoded texel inside an sRGB FBO. Default
* filter = identity, so the non-sRGB path is byte-identical. */
return iso_apply_primitive_color_filter(al_map_rgba_f(r, g, b, 1.0f));
}
static void _draw_tile_warped(ALLEGRO_BITMAP* bmp,
const float vx[4],
const float vy[4],
int tile_w,
int tile_h,
ALLEGRO_COLOR tint) {
/* al_draw_prim samples u/v against the PARENT texture and does NOT
* auto-translate for a sub-bitmap (unlike al_draw_*_bitmap). When tiles
* are packed into a terrain atlas (each tile an al_create_sub_bitmap),
* passing sub-local UVs [0..tile_w] samples the atlas ORIGIN, which is
* transparent padding, so warped (sloped) tiles render BLACK. Resolve
* the parent + add the sub-bitmap's atlas offset to every UV and draw
* against the parent so the warp samples the tile's real atlas region.
* For a standalone (non-sub) bitmap the offsets are 0 and parent == bmp,
* so this is byte-identical to the pre-atlas behaviour. */
ALLEGRO_BITMAP* tex = al_get_parent_bitmap(bmp);
float ox, oy;
if (tex) {
ox = (float)al_get_bitmap_x(bmp);
oy = (float)al_get_bitmap_y(bmp);
} else {
tex = bmp;
ox = 0.0f;
oy = 0.0f;
}
float un = ox + (float)tile_w / 2.0f, vn = oy + 0.0f;
float ue = ox + (float)tile_w, ve = oy + (float)tile_h / 2.0f;
float us = ox + (float)tile_w / 2.0f, vs = oy + (float)tile_h;
float uw = ox + 0.0f, vw = oy + (float)tile_h / 2.0f;
ALLEGRO_VERTEX vtx[6];
vtx[0].x = vx[0];
vtx[0].y = vy[0];
vtx[0].z = 0;
vtx[0].u = un;
vtx[0].v = vn;
vtx[0].color = tint;
vtx[1].x = vx[1];
vtx[1].y = vy[1];
vtx[1].z = 0;
vtx[1].u = ue;
vtx[1].v = ve;
vtx[1].color = tint;
vtx[2].x = vx[2];
vtx[2].y = vy[2];
vtx[2].z = 0;
vtx[2].u = us;
vtx[2].v = vs;
vtx[2].color = tint;
vtx[3].x = vx[0];
vtx[3].y = vy[0];
vtx[3].z = 0;
vtx[3].u = un;
vtx[3].v = vn;
vtx[3].color = tint;
vtx[4].x = vx[2];
vtx[4].y = vy[2];
vtx[4].z = 0;
vtx[4].u = us;
vtx[4].v = vs;
vtx[4].color = tint;
vtx[5].x = vx[3];
vtx[5].y = vy[3];
vtx[5].z = 0;
vtx[5].u = uw;
vtx[5].v = vw;
vtx[5].color = tint;
al_draw_prim(vtx, NULL, tex, 0, 6, ALLEGRO_PRIM_TRIANGLE_LIST);
}
static void _draw_segment_sides(const ISO_MAP* map,
float east_x,
float east_y,
float south_x,
float south_y,
float west_x,
float west_y,
int mx,
int my,
int seg_bottom,
int seg_top,
float lift,
float height_brightness,
int wall_terrain_override) {
int base = (wall_terrain_override >= 0) ? wall_terrain_override
: iso_map_get_terrain(map, mx, my);
float east_shade = 0.55f;
float south_shade = 0.35f;
float cr, cg, cb;
switch (base) {
case 0:
cr = 0.7f;
cg = 0.25f;
cb = 0.1f;
break;
case 1:
cr = 0.5f;
cg = 0.45f;
cb = 0.35f;
break;
case 2:
cr = 0.6f;
cg = 0.55f;
cb = 0.45f;
break;
default:
cr = 0.4f;
cg = 0.35f;
cb = 0.3f;
break;
}
ALLEGRO_COLOR east_col = iso_apply_primitive_color_filter(
al_map_rgba_f(cr * east_shade * height_brightness,
cg * east_shade * height_brightness,
cb * east_shade * height_brightness, 1.0f));
ALLEGRO_COLOR south_col = iso_apply_primitive_color_filter(
al_map_rgba_f(cr * south_shade * height_brightness,
cg * south_shade * height_brightness,
cb * south_shade * height_brightness, 1.0f));
/* Keep PASS 1 on ONE blender throughout to avoid held-batch
* cross-tile contamination. Cliff wall fills use alpha=1.0 ->
* premul == straight, and edge lines are RGB=0 -> premul ==
* straight, so this switch is a visual no-op. */
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
int east_face_h, south_face_h;
if (seg_bottom == 0) {
/* Ground segment: use neighbor-aware wall height (existing logic) */
int h_e = iso_map_get_height(map, mx + 1, my);
int h_s = iso_map_get_height(map, mx, my + 1);
int h_se = iso_map_get_height(map, mx + 1, my + 1);
east_face_h = seg_top - (h_e < h_se ? h_e : h_se);
// cppcheck-suppress redundantAssignment ; fallback chain: min(h_e,h_se) -> h_e -> h_se
if (east_face_h < 1) east_face_h = seg_top - h_e;
if (east_face_h < 1) east_face_h = seg_top - h_se;
south_face_h = seg_top - (h_s < h_se ? h_s : h_se);
// cppcheck-suppress redundantAssignment ; fallback chain: min(h_s,h_se) -> h_s -> h_se
if (south_face_h < 1) south_face_h = seg_top - h_s;
if (south_face_h < 1) south_face_h = seg_top - h_se;
} else {
/* Floating segment: full wall from bottom to top */
east_face_h = seg_top - seg_bottom;
south_face_h = seg_top - seg_bottom;
}
/* No sub-pixel expansion needed: all vertices come from the canonical
* _iso_corner_to_screen formula, so adjacent tiles sharing an edge
* produce bit-identical vertex positions. */
/* Query the client-registered jitter source. Returns (0, 0) when
* no jitter source is active. When non-zero, we subtract it from
* every line endpoint so the line lands at the same screen pixel
* each frame regardless of where in the TAA jitter cycle we are.
* Filled polygon faces deliberately KEEP the jitter: they are
* the larger draws TAA's history blend smooths perceptually. */
float jx = 0.0f, jy = 0.0f;
iso_apply_line_jitter_compensation(&jx, &jy);
if (east_face_h > 0) {
float drop = (float)east_face_h * lift;
float v[8] = {east_x, east_y, south_x, south_y,
south_x, south_y + drop,
east_x, east_y + drop};
al_draw_filled_polygon(v, 4, east_col);
ALLEGRO_COLOR edge = al_map_rgba(0, 0, 0, 80);
al_draw_line(east_x - jx, east_y + drop - jy,
south_x - jx, south_y + drop - jy,
edge, 1.0f);
}
if (south_face_h > 0) {
float drop = (float)south_face_h * lift;
float v[8] = {south_x, south_y, west_x, west_y,
west_x, west_y + drop,
south_x, south_y + drop};
al_draw_filled_polygon(v, 4, south_col);
ALLEGRO_COLOR edge = al_map_rgba(0, 0, 0, 80);
al_draw_line(west_x - jx, west_y + drop - jy,
south_x - jx, south_y + drop - jy,
edge, 1.0f);
}
}
static void _draw_segment_underside(ALLEGRO_BITMAP* bmp,
float base_dx,
float base_dy,
float phw,
float phh,
float tile_draw_w,
float tile_draw_h,
int tile_w,
int tile_h) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
/* Draw the tile bitmap with a darkened tint (65% brightness) */
ALLEGRO_COLOR tint = al_map_rgba_f(0.65f, 0.65f, 0.65f, 1.0f);
(void)phw;
(void)phh;
al_draw_tinted_scaled_bitmap(bmp, tint,
0, 0, (float)tile_w, (float)tile_h,
base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
}
static int _iso_object_check_occlusion(const ISO_MAP* map,
float obj_fx,
float obj_fy,
float obj_fz,
float obj_sx,
float obj_sy,
float sprite_h,
float sprite_half_w,
float cam_px,
float cam_py,
float zoom,
float* out_clip_y) {
/* Hoist projection constants out of the inner loop. */
const float proj_hw = map->proj.half_w;
const float proj_hh = map->proj.half_h;
const float proj_lift = map->proj.tile_lift;
float phw = proj_hw * zoom;
float phh = proj_hh * zoom;
float lift = proj_lift * zoom;
/* Entity head screen Y: sprite_h world units above feet */
float head_sy = obj_sy - sprite_h * lift;
/* Entity horizontal extent (screen pixels) */
float ent_left = obj_sx - sprite_half_w * zoom;
float ent_right = obj_sx + sprite_half_w * zoom;
int base_mx = (int)floorf(obj_fx);
int base_my = (int)floorf(obj_fy);
int check_range = map->max_height + 2;
int found = 0;
float best_clip_y = 1e9f;
for (int dy = -1; dy <= check_range; dy++) {
for (int dx = -1; dx <= check_range; dx++) {
/* Only check tiles drawn after the object in painter's order.
* Filter by isometric depth (dx+dy) not just grid position. */
int tdepth = dx + dy;
if (tdepth < 0) continue;
if (tdepth == 0 && dy < 0) continue;
if (tdepth == 0 && dy == 0 && dx <= 0) continue;
int tmx = base_mx + dx;
int tmy = base_my + dy;
if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height) continue;
int th = iso_map_get_height(map, tmx, tmy);
/* tile must reach above the entity's feet to potentially occlude */
if ((float)th <= obj_fz) continue;
/* inlined iso_map_to_screen(map, tmx, tmy, th, &tsx, &tsy) */
float tsx = ((float)tmx - (float)tmy) * proj_hw + proj_hw;
float tsy = ((float)tmx + (float)tmy) * proj_hh - (float)th * proj_lift;
float tile_dx = tsx * zoom + cam_px;
float tile_dy = tsy * zoom + cam_py;
/* tile screen bounding box */
float tile_left = tile_dx;
float tile_right = tile_dx + 2.0f * phw;
float tile_top = tile_dy;
float tile_bot = tile_dy + 2.0f * phh + (float)th * lift;
/* Check horizontal overlap (entity range vs tile range) */
if (ent_right < tile_left || ent_left > tile_right) continue;
/* Check vertical overlap (any part of entity behind tile) */
if (tile_top <= obj_sy && tile_bot >= head_sy) {
found = 1;
/* Track north vertex of occluding tile for clip-Y */
float nx, ny;
_iso_corner_to_screen(proj_hw, proj_hh, proj_lift,
tmx, tmy, (float)th,
cam_px, cam_py, zoom, &nx, &ny);
if (ny < best_clip_y) best_clip_y = ny;
}
}
}
if (out_clip_y) *out_clip_y = best_clip_y;
return found;
}
static void _draw_height_borders(const ISO_MAP* map,
int mx,
int my,
int seg_idx,
int seg_bottom,
int seg_top,
int has_underside_face,
const float vx[4],
const float vy[4],
float lift) {
ISO_VISIBLE_EDGES edges = iso_map_get_visible_edges_segment(map, mx, my, seg_idx);
if (!edges.draw_nw && !edges.draw_ne && !edges.draw_se && !edges.draw_sw && !(has_underside_face && edges.draw_underside))
return;
/* Subtract the client-registered jitter (e.g. the consumer's TAA
* sub-pixel offset) from every line endpoint so these decorative
* borders stay pixel-stable across frames while sprites and
* filled faces continue to inherit the jitter. Returns (0, 0)
* when no jitter source is active. */
float jx = 0.0f, jy = 0.0f;
iso_apply_line_jitter_compensation(&jx, &jy);
ALLEGRO_COLOR border_col = al_map_rgba(0, 0, 0, 200);
float thickness = 1.5f;
/* Top face edges */
if (edges.draw_se)
al_draw_line(vx[1] - jx, vy[1] - jy,
vx[2] - jx, vy[2] - jy, border_col, thickness);
if (edges.draw_sw)
al_draw_line(vx[2] - jx, vy[2] - jy,
vx[3] - jx, vy[3] - jy, border_col, thickness);
if (edges.draw_nw)
al_draw_line(vx[0] - jx, vy[0] - jy,
vx[3] - jx, vy[3] - jy, border_col, thickness);
if (edges.draw_ne)
al_draw_line(vx[0] - jx, vy[0] - jy,
vx[1] - jx, vy[1] - jy, border_col, thickness);
/* Vertical corner lines.
*
* For each drawn edge where seg_top > neighbor_top (A is higher):
* Draw a vertical line at BOTH endpoints of that edge,
* from tileToScreen(vertex, seg_top) down to
* tileToScreen(vertex, neighbor_top).
* Exception: NEVER draw at the North vertex (vx[0]/vy[0]).
*
* If seg_top <= neighbor_top: no vertical lines (wall belongs to N).
*
* vx[]/vy[] are already at tileToScreen(vertex, seg_top).
* The bottom endpoint is (seg_top - HN) * lift pixels below.
*
* Diamond vertices: N=0, E=1, S=2, W=3.
* Edge endpoints: NE=(N,E) SE=(E,S) SW=(S,W) NW=(W,N)
*
* Multiple edges may want to draw at the same vertex with different
* drops. We draw the longest line (smallest HN) at each vertex
* to avoid duplicate shorter lines.
*/
{
/* Per-vertex: track the largest drop (longest line) requested.
*
* CONTINUITY CHECK: a vertical corner line at a vertex is only
* drawn if the wall face ENDS at that vertex, i.e. the adjacent
* tile in the wall's direction is NOT at the same height.
* If the adjacent tile IS at the same height, the wall surface
* continues past the vertex and the line is interior (hidden).
*
* For the east-facing wall (SE edge):
* E vertex: wall continues northward if NE neighbor same height -> skip
* S vertex: wall continues southward if SW neighbor same height -> skip
* For the south-facing wall (SW edge):
* S vertex: wall continues eastward if SE neighbor same height -> skip
* W vertex: wall continues westward if NW neighbor same height -> skip
* For NE edge -> E vertex: wall continues if SE neighbor same height
* For NW edge -> W vertex: wall continues if SW neighbor same height
*/
float drop_e = 0, drop_s = 0, drop_w = 0;
int nt_nw = edges.neighbor_top_nw;
int nt_ne = edges.neighbor_top_ne;
int nt_se = edges.neighbor_top_se;
int nt_sw = edges.neighbor_top_sw;
/* Clamp neighbor tops to seg_bottom: vertical lines must not
* extend below the segment's own base (floating segments). */
int bot = seg_bottom;
int cl_nw = nt_nw > bot ? nt_nw : bot;
int cl_ne = nt_ne > bot ? nt_ne : bot;
int cl_se = nt_se > bot ? nt_se : bot;
int cl_sw = nt_sw > bot ? nt_sw : bot;
/* NE edge (N,E): only E vertex (N is never drawn) */
if (edges.draw_ne && seg_top > cl_ne) {
/* E vertex: east wall continues if SE neighbor same height */
if (nt_se != seg_top) {
float d = (float)(seg_top - cl_ne) * lift;
if (d > drop_e) drop_e = d;
}
}
/* SE edge (E,S): E and S vertices */
if (edges.draw_se && seg_top > cl_se) {
float d = (float)(seg_top - cl_se) * lift;
/* E vertex: east wall continues northward if NE neighbor same height */
if (nt_ne != seg_top) {
if (d > drop_e) drop_e = d;
}
/* S vertex: east wall continues southward if SW neighbor same height */
if (nt_sw != seg_top) {
if (d > drop_s) drop_s = d;
}
}
/* SW edge (S,W): S and W vertices */
if (edges.draw_sw && seg_top > cl_sw) {
float d = (float)(seg_top - cl_sw) * lift;
/* S vertex: south wall continues eastward if SE neighbor same height */
if (nt_se != seg_top) {
if (d > drop_s) drop_s = d;
}
/* W vertex: south wall continues westward if NW neighbor same height */
if (nt_nw != seg_top) {
if (d > drop_w) drop_w = d;
}
}
/* NW edge (W,N): only W vertex (N is never drawn) */
if (edges.draw_nw && seg_top > cl_nw) {
/* W vertex: south wall continues if SW neighbor same height */
if (nt_sw != seg_top) {
float d = (float)(seg_top - cl_nw) * lift;
if (d > drop_w) drop_w = d;
}
}
if (drop_e > 0)
al_draw_line(vx[1] - jx, vy[1] - jy,
vx[1] - jx, vy[1] + drop_e - jy,
border_col, thickness);
if (drop_s > 0)
al_draw_line(vx[2] - jx, vy[2] - jy,
vx[2] - jx, vy[2] + drop_s - jy,
border_col, thickness);
if (drop_w > 0)
al_draw_line(vx[3] - jx, vy[3] - jy,
vx[3] - jx, vy[3] + drop_w - jy,
border_col, thickness);
}
/* Underside face border (floating segments only).
* The underside diamond is at seg_bottom, offset below the top face
* by (seg_top - seg_bottom) * lift pixels. Only drawn if the
* underside face itself was rendered (has_underside_face).
* Only SE and SW edges, NW and NE are always hidden behind the
* segment's own side walls from the camera angle. */
if (has_underside_face && edges.draw_underside) {
float dy = (float)(seg_top - seg_bottom) * lift;
/* SE edge */
al_draw_line(vx[1] - jx, vy[1] + dy - jy,
vx[2] - jx, vy[2] + dy - jy,
border_col, thickness);
/* SW edge */
al_draw_line(vx[2] - jx, vy[2] + dy - jy,
vx[3] - jx, vy[3] + dy - jy,
border_col, thickness);
}
}
void iso_map_draw(const ISO_MAP* map,
ALLEGRO_BITMAP** tile_bitmaps,
ALLEGRO_BITMAP*** transition_tiles,
int num_masks,
ALLEGRO_BITMAP** overlay_bitmaps,
int num_overlay_tiles,
float cam_px,
float cam_py,
float zoom,
int screen_w,
int screen_h,
int player_mode,
N_ISO_OBJECT* objects,
int num_objects) {
__n_assert(map, return);
__n_assert(tile_bitmaps, return);
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
float hw0 = map->proj.half_w;
float hh0 = map->proj.half_h;
float tl0 = map->proj.tile_lift;
float phw = hw0 * zoom;
float phh = hh0 * zoom;
float lift = tl0 * zoom;
float tile_draw_w = 2.0f * phw;
float tile_draw_h = 2.0f * phh;
/* cam_px / cam_py are pre-computed pixel offsets provided by the caller.
* The caller may pass either integer-snapped (`floorf(cam_x * zoom)`) or
* sub-pixel (`cam_x * zoom`) values. Sub-pixel is preferred when paired
* with MIN_LINEAR bilinear sampling for smooth camera motion. Integer
* snap pairs with NEAREST sampling for crisp pixel-art but produces
* "world jumps by 1 pixel" stutter during motion. Chunk pixel offsets
* are added on top of whatever value the caller passes: they remain
* integer at zoom = 1 regardless. */
int tile_w = tile_bitmaps[0] ? al_get_bitmap_width(tile_bitmaps[0]) : 64;
int tile_h = tile_bitmaps[0] ? al_get_bitmap_height(tile_bitmaps[0]) : 32;
int num_edge_masks = ISO_NUM_EDGE_MASKS;
double _iso_t_setup = al_get_time();
/* Build segment draw order.
*
* The draw_order is cached on the ISO_MAP and only rebuilt when
* the segment/height layout actually changes (`draw_order_dirty`
* is set by iso_map_set_segments). For a static-terrain scene
* this drops to a zero-work hit per frame. obj_order / obj_sx_arr
* / obj_sy_arr stay file-static because objects' world positions
* change every frame and the buffer just needs to be big enough;
* per-map ownership isn't required. */
int max_draw_entries = map->width * map->height * ISO_MAX_SEGMENTS_PER_TILE;
ISO_MAP* mut_map = (ISO_MAP*)map; /* the cache fields are deliberately writable */
if (mut_map->cached_draw_order_cap < max_draw_entries) {
ISO_DRAW_ENTRY* grown = (ISO_DRAW_ENTRY*)realloc(
mut_map->cached_draw_order,
(size_t)max_draw_entries * sizeof(ISO_DRAW_ENTRY));
if (!grown) return;
mut_map->cached_draw_order = grown;
mut_map->cached_draw_order_cap = max_draw_entries;
mut_map->draw_order_dirty = 1;
}
if (mut_map->draw_order_dirty) {
mut_map->cached_draw_order_count = iso_map_build_draw_order(
map, mut_map->cached_draw_order, mut_map->cached_draw_order_cap);
mut_map->draw_order_dirty = 0;
}
ISO_DRAW_ENTRY* draw_order = mut_map->cached_draw_order;
int num_entries = mut_map->cached_draw_order_count;
/* Pre-process objects: compute screen positions and sort by depth.
* Sort key matches segment draw order: (floor(fz), my, mx). */
int nobj = (objects && num_objects > 0) ? num_objects : 0;
static int* obj_order = NULL;
static float* obj_sx_arr = NULL;
static float* obj_sy_arr = NULL;
static int obj_buf_cap = 0;
if (nobj > obj_buf_cap) {
int new_cap = obj_buf_cap > 0 ? obj_buf_cap : 64;
while (new_cap < nobj) new_cap *= 2;
int* g1 = (int*)realloc(obj_order, (size_t)new_cap * sizeof(int));
float* g2 = (float*)realloc(obj_sx_arr, (size_t)new_cap * sizeof(float));
float* g3 = (float*)realloc(obj_sy_arr, (size_t)new_cap * sizeof(float));
if (!g1 || !g2 || !g3) {
n_log(LOG_ERR, "iso_map_draw: object buffer realloc failed");
/* Keep whatever grew successfully; on next frame the
* remaining one(s) will be retried. */
if (g1) obj_order = g1;
if (g2) obj_sx_arr = g2;
if (g3) obj_sy_arr = g3;
nobj = 0;
} else {
obj_order = g1;
obj_sx_arr = g2;
obj_sy_arr = g3;
obj_buf_cap = new_cap;
}
}
if (nobj > 0) {
for (int i = 0; i < nobj; i++) {
obj_order[i] = i;
objects[i].is_occluded = 0;
objects[i].occlude_clip_y = 1e9f;
float wsx, wsy;
iso_map_to_screen_f(map, objects[i].fx, objects[i].fy, objects[i].fz, &wsx, &wsy);
obj_sx_arr[i] = wsx * zoom + cam_px;
obj_sy_arr[i] = wsy * zoom + cam_py;
}
/* insertion sort by (depth, floor(fz), my, mx) to match segment draw order */
for (int i = 1; i < nobj; i++) {
int key = obj_order[i];
int key_fz = (int)floorf(objects[key].fz);
int key_my = (int)floorf(objects[key].fy);
int key_mx = (int)floorf(objects[key].fx);
int key_depth = key_my + key_mx;
int j = i - 1;
while (j >= 0) {
int cj = obj_order[j];
int cj_fz = (int)floorf(objects[cj].fz);
int cj_my = (int)floorf(objects[cj].fy);
int cj_mx = (int)floorf(objects[cj].fx);
int cj_depth = cj_my + cj_mx;
if (cj_depth > key_depth ||
(cj_depth == key_depth && cj_fz > key_fz) ||
(cj_depth == key_depth && cj_fz == key_fz && cj_my > key_my) ||
(cj_depth == key_depth && cj_fz == key_fz && cj_my == key_my && cj_mx > key_mx)) {
obj_order[j + 1] = obj_order[j];
j--;
} else {
break;
}
}
obj_order[j + 1] = key;
}
}
n_iso_setup_us += (al_get_time() - _iso_t_setup) * 1.0e6;
double _iso_t_pass1 = al_get_time();
/* PASS 1: Ground segments only (bottom == 0), no objects
* Draw all ground-level tiles first so they can never overdraw
* elevated content (entities on bridges, floating segments, etc.).
*
* Wrap the entire pass in al_hold_bitmap_drawing. The previous
* frame-wide wrap regressed because Pass 2 interleaves tile draws
* with object-callback sprite draws (each from a different source
* bitmap), forcing flushes. Pass 1 has no object callbacks, it's
* pure tile draws from whatever source the caller provided. When
* the caller groups tiles + transitions + overlays into one atlas,
* consecutive draws share a source texture and accumulate cleanly
* between state-change flushes. The internal al_set_blender calls
* still flush, but runs of same-blender draws within a blender
* regime now batch.
*
* `blender_is_one` tracks whether the current blender is
* ADD/ONE/INV_ALPHA. Every set to ONE flips it true; every
* set to ALPHA (walls, overlay, borders, hover, grid) flips it
* false. This lets us skip the per-tile "reset to ONE" at the
* head of each iteration when no ALPHA block ran in the
* previous iteration, the common case for uninstrumented
* ground tiles. */
int blender_is_one = 1; /* set by line 2582 above the pass */
/* al_hold_bitmap_drawing wrap around PASS 1, opt-in via
* NILOREA_PASS1_HOLD=1. Wrapping the pass batches consecutive
* bitmap draws (~2 ms speedup) but historically introduced
* intermittent darker tile-mask diamond edges at the boundaries.
* After the bitmap/primitive interleave carve-out (see below)
* the wrap is correct end-to-end, but it remains opt-in by
* environment variable for A/B perf comparison. */
static int s_hold_pass1 = -1;
static int s_hold_per_tile = -1;
static int s_no_transitions = -1;
static int s_no_overlay = -1;
static int s_no_height_borders = -1;
static int s_no_hover_grid = -1;
static int s_tile_limit = -1;
static int s_force_alpha = -1;
if (s_hold_pass1 < 0) {
/* Default is ON; set NILOREA_PASS1_HOLD=0 to opt OUT. */
const char* e = getenv("NILOREA_PASS1_HOLD");
s_hold_pass1 = (e && e[0] == '0') ? 0 : 1;
}
/* PASS 1 instrumentation knobs. Each is env-gated, default-off so
* production builds are unchanged. Useful when investigating
* rendering artefacts.
*
* NILOREA_PASS1_HOLD_PER_TILE=1
* Replace the pass-wide al_hold_bitmap_drawing(true/false)
* wrap with a per-tile cycle. Each tile becomes its own
* isolated batch.
*
* NILOREA_PASS1_NO_TRANSITIONS=1
* Skip the terrain-transitions loop entirely.
*
* NILOREA_PASS1_NO_OVERLAY=1
* Skip the overlay-bitmap draw (ALPHA blender).
*
* NILOREA_PASS1_NO_HEIGHT_BORDERS=1
* Skip the height-borders ALPHA draw.
*
* NILOREA_PASS1_NO_HOVER_GRID=1
* Skip the hover-highlight and grid-overlay ALPHA draws.
*
* NILOREA_PASS1_TILE_LIMIT=N
* Render at most N tiles in PASS 1, then break.
*
* NILOREA_PASS1_FORCE_ALPHA=1
* Force ALPHA blender (instead of ONE) for the base-tile
* and transitions draws. */
if (s_hold_per_tile < 0) {
const char* e = getenv("NILOREA_PASS1_HOLD_PER_TILE");
s_hold_per_tile = (e && e[0] == '1') ? 1 : 0;
}
if (s_no_transitions < 0) {
const char* e = getenv("NILOREA_PASS1_NO_TRANSITIONS");
s_no_transitions = (e && e[0] == '1') ? 1 : 0;
}
if (s_no_overlay < 0) {
const char* e = getenv("NILOREA_PASS1_NO_OVERLAY");
s_no_overlay = (e && e[0] == '1') ? 1 : 0;
}
if (s_no_height_borders < 0) {
const char* e = getenv("NILOREA_PASS1_NO_HEIGHT_BORDERS");
s_no_height_borders = (e && e[0] == '1') ? 1 : 0;
}
if (s_no_hover_grid < 0) {
const char* e = getenv("NILOREA_PASS1_NO_HOVER_GRID");
s_no_hover_grid = (e && e[0] == '1') ? 1 : 0;
}
if (s_tile_limit < 0) {
const char* e = getenv("NILOREA_PASS1_TILE_LIMIT");
s_tile_limit = (e && e[0] >= '0' && e[0] <= '9') ? atoi(e) : 0;
}
if (s_force_alpha < 0) {
const char* e = getenv("NILOREA_PASS1_FORCE_ALPHA");
s_force_alpha = (e && e[0] == '1') ? 1 : 0;
}
/* If per-tile hold is requested, force the pass-wide hold OFF:
* the per-tile cycle below opens/closes its own holds. */
int effective_hold_pass = s_hold_pass1 && !s_hold_per_tile;
if (effective_hold_pass) {
al_hold_bitmap_drawing(true);
/* Batch warmup. The first batched draw after
* al_hold_bitmap_drawing(true) can hit a slightly different
* fragment-shading path than subsequent draws within the same
* held context, producing darker fractional-alpha tile-mask
* edges on the early painter-order tiles.
*
* Defence: prepend a fully-transparent 1-pixel draw at an
* offscreen position so the suspect "first draw in hold
* cycle" is absorbed into an invisible pixel. The actual
* tile draws then all run from the second-draw-onwards
* state, which is the consistent one. Zero visual cost
* (transparent + offscreen) and negligible perf cost. */
if (tile_bitmaps[0]) {
al_draw_tinted_scaled_bitmap(tile_bitmaps[0],
al_map_rgba(0, 0, 0, 0),
0, 0, 1, 1,
-100, -100, 1, 1, 0);
}
}
int _p1_tiles_drawn = 0;
for (int di = 0; di < num_entries; di++) {
if (draw_order[di].bottom > 0) continue; /* skip elevated */
/* Tile limit knob: stop after N ground tiles drawn. */
if (s_tile_limit > 0 && _p1_tiles_drawn >= s_tile_limit) break;
_p1_tiles_drawn++;
/* Per-tile hold cycle: each tile is its own isolated batch.
* The pass-wide hold is suppressed when this knob is on. */
if (s_hold_per_tile) al_hold_bitmap_drawing(true);
int mx = draw_order[di].mx;
int my = draw_order[di].my;
int seg_top = draw_order[di].top;
int h = seg_top;
int base = iso_map_get_terrain(map, mx, my);
/* `al_hold_bitmap_drawing` only batches bitmap draws.
* `al_draw_line` / `al_draw_filled_polygon` (primitives addon)
* draw IMMEDIATELY regardless of hold. So under the pass-wide
* hold:
* - tile base bitmap is queued in batch
* - wall polygon is drawn now at the screen position
* - border line is drawn now
* - more tiles ...
* - end of pass: hold(false) flushes all queued bitmaps
*
* The bitmap flush at end draws on TOP of the primitives,
* hiding height borders / wall edges. Adjacent tiles' diamond
* edges (where transitions + border lines render at the same
* sub-pixel position with bilinear vs line-AA coverage) also
* produce a doubled-outline effect during camera motion.
*
* Fix: for tiles that need primitive draws (elevated walls,
* height borders, hover highlight, grid overlay), step OUT of
* the held batch: flush the queued bitmaps, draw the tile
* fully in immediate mode (bitmaps + primitives interleaved
* correctly), then re-enter the held batch for the next
* batchable tile. The bulk of tiles (flat ground, no overlay,
* no hover, no grid) stays inside the held batch unchanged. */
/* Smooth-mode slope detection, computed HERE, before the hold-batch
* decision below, because a sloped tile's top is drawn with
* al_draw_prim (a primitives-addon call) which renders INCORRECTLY
* inside al_hold_bitmap_drawing(true): the queued-batch state scales
* + misplaces the warped quad (the "flat<->slope transition tiles black
* out / shrink toward screen centre" bug). Such tiles must step out
* of the held batch exactly like wall/hover/grid tiles already do. A
* FLAT tile takes the batch-safe al_draw_tinted_scaled_bitmap path and
* stays held. h_n/h_e/h_s/h_w are reused by the vx/vy block below so
* iso_map_smooth_corner_heights is called only once per tile. */
float h_n = (float)h, h_e = (float)h, h_s = (float)h, h_w = (float)h;
int tile_is_flat = 1;
if (map->smooth_height) {
iso_map_smooth_corner_heights(map, mx, my, &h_n, &h_e, &h_s, &h_w);
tile_is_flat = (h_n == h_e && h_e == h_s && h_s == h_w);
}
int tile_needs_primitives = (seg_top > 0) ||
(map->smooth_height && !tile_is_flat) ||
(!s_no_hover_grid && mx == map->hover_mx && my == map->hover_my) ||
(map->show_grid && !s_no_hover_grid);
int unbatched_tile = effective_hold_pass && tile_needs_primitives;
if (unbatched_tile) {
al_hold_bitmap_drawing(false);
}
/* Per-segment tile overrides: upper_tile for top face, lower_tile for walls */
int top_terrain = base;
int wall_terrain = base;
{
int si = draw_order[di].seg_idx;
int tidx = my * map->width + mx;
if (map->segments && tidx >= 0 && tidx < map->width * map->height &&
si >= 0 && si < map->segments[tidx].count) {
int ut = map->segments[tidx].segs[si].upper_tile;
int lt = map->segments[tidx].segs[si].lower_tile;
if (ut >= 0 && ut < map->num_terrains) top_terrain = ut;
if (lt >= 0 && lt < map->num_terrains) wall_terrain = lt;
}
}
float vx[4], vy[4];
float base_dx, base_dy;
if (map->smooth_height) {
/* h_n/h_e/h_s/h_w + tile_is_flat already computed above. */
_iso_corner_to_screen(hw0, hh0, tl0, mx, my, h_n, cam_px, cam_py, zoom, &vx[0], &vy[0]);
_iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, h_e, cam_px, cam_py, zoom, &vx[1], &vy[1]);
_iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, h_s, cam_px, cam_py, zoom, &vx[2], &vy[2]);
_iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, h_w, cam_px, cam_py, zoom, &vx[3], &vy[3]);
base_dx = vx[3];
base_dy = vy[0];
} else {
_iso_corner_to_screen(hw0, hh0, tl0, mx, my, (float)h, cam_px, cam_py, zoom, &vx[0], &vy[0]);
_iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &vx[1], &vy[1]);
_iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &vx[2], &vy[2]);
_iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &vx[3], &vy[3]);
base_dx = vx[3];
base_dy = vy[0];
}
/* Culling */
float min_vy = fminf(fminf(vy[0], vy[1]), fminf(vy[2], vy[3]));
float max_vy = fmaxf(fmaxf(vy[0], vy[1]), fmaxf(vy[2], vy[3]));
float max_side = fmaxf((float)h, (float)map->max_height) * lift;
if (vx[1] < 0 || vx[3] > (float)screen_w) continue;
if (max_vy + max_side < 0 || min_vy > (float)screen_h) continue;
/* Height-based tint + ambient + dynamic lighting for this segment */
float amb_r = map->ambient_r > 0.0f ? map->ambient_r : 1.0f;
float amb_g = map->ambient_g > 0.0f ? map->ambient_g : 1.0f;
float amb_b = map->ambient_b > 0.0f ? map->ambient_b : 1.0f;
float dyn_r = 0.0f, dyn_g = 0.0f, dyn_b = 0.0f;
if (map->dynamic_light_map) {
int lidx = (my * map->width + mx) * 3;
dyn_r = map->dynamic_light_map[lidx];
dyn_g = map->dynamic_light_map[lidx + 1];
dyn_b = map->dynamic_light_map[lidx + 2];
}
ALLEGRO_COLOR seg_tint = _compute_tile_tint(seg_top, map->max_height,
map->height_tint_intensity,
amb_r, amb_g, amb_b,
dyn_r, dyn_g, dyn_b);
float seg_brightness = 1.0f;
if (map->height_tint_intensity > 0.0f && map->max_height > 0) {
float ratio = (float)seg_top / (float)map->max_height;
seg_brightness = 1.0f - map->height_tint_intensity * (1.0f - ratio);
}
/* Factor in ambient for wall brightness */
seg_brightness *= (amb_r + amb_g + amb_b) / 3.0f;
float wall_dyn = (dyn_r + dyn_g + dyn_b) / 3.0f;
seg_brightness += wall_dyn;
if (seg_brightness > 1.0f) seg_brightness = 1.0f;
/* Top surface: ALWAYS set the blender at the start of every
* tile, dropping any cross-tile cache. Under
* `al_hold_bitmap_drawing(true)`, `al_set_blender` flushes the
* current held batch and starts a new one. If the call were
* skipped when the previous tile ended on ONE, the held
* batch's flush boundaries would depend on which tiles had
* overlays/walls/hover/grid, and that set shifts when the
* camera moves. Inconsistent flush boundaries cause
* inconsistent GPU-rasterised pixel coverage on shared
* diamond edges, producing flickering darker borders during
* camera motion. Forcing the call here makes the flush
* boundary deterministic at every tile, trading O(1000) extra
* al_set_blender calls per frame for stable rasterisation.
* The within-tile transitions loop keeps its conditional `if
* (!blender_is_one)` skip because within a tile the state is
* fresh and skipping is safe. */
if (s_force_alpha) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
blender_is_one = 0;
} else {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
blender_is_one = 1;
}
if (map->smooth_height && !tile_is_flat) {
_draw_tile_warped(tile_bitmaps[top_terrain], vx, vy, tile_w, tile_h, seg_tint);
} else {
/* Flat tile (or CUT mode): the al_draw_prim warp samples atlas
* sub-bitmaps differently from al_draw_tinted_scaled_bitmap, which
* renders flat ground tiles wrong once tiles are packed into the
* terrain atlas. For a flat tile the warp geometry is identical to
* this scaled-bitmap draw anyway (verified: corner span == tdw*tdh),
* so route flat tiles through the proven atlas-correct path and
* reserve the warp for genuinely sloped tiles. */
al_draw_tinted_scaled_bitmap(tile_bitmaps[top_terrain], seg_tint,
0, 0, (float)tile_w, (float)tile_h,
base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
}
/* Cliff walls (internally sets ONE blender). */
if (seg_top > 0) {
if (map->smooth_height) {
int h_east = iso_map_get_height(map, mx + 1, my);
int h_south = iso_map_get_height(map, mx, my + 1);
int h_se = iso_map_get_height(map, mx + 1, my + 1);
if ((h - h_east > map->smooth_slope_max) ||
(h - h_south > map->smooth_slope_max) ||
(h - h_se > map->smooth_slope_max)) {
float ce_x, ce_y, cs_x, cs_y, cw_x, cw_y;
_iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &ce_x, &ce_y);
_iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &cs_x, &cs_y);
_iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &cw_x, &cw_y);
_draw_segment_sides(map, ce_x, ce_y, cs_x, cs_y, cw_x, cw_y,
mx, my, 0, seg_top, lift, seg_brightness, wall_terrain);
/* blender_is_one stays 1: function leaves state on ONE */
}
} else {
_draw_segment_sides(map, vx[1], vy[1], vx[2], vy[2], vx[3], vy[3],
mx, my, 0, seg_top, lift, seg_brightness, wall_terrain);
/* blender_is_one stays 1 */
}
}
/* Terrain transitions (same tint as base tile; ONE blender) */
if (transition_tiles && !s_no_transitions) {
int edge_bits_arr[16];
int corner_bits_arr[16];
int nt = map->num_terrains < 16 ? map->num_terrains : 16;
memset(edge_bits_arr, 0, sizeof(int) * (size_t)nt);
memset(corner_bits_arr, 0, sizeof(int) * (size_t)nt);
iso_map_calc_transitions_full(map, mx, my, edge_bits_arr, corner_bits_arr);
for (int t = base + 1; t < nt; t++) {
if (edge_bits_arr[t] != 0 && transition_tiles[t] &&
transition_tiles[t][edge_bits_arr[t]] != NULL) {
if (!blender_is_one && !s_force_alpha) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
blender_is_one = 1;
}
if (map->smooth_height && !tile_is_flat) {
_draw_tile_warped(transition_tiles[t][edge_bits_arr[t]],
vx, vy, tile_w, tile_h, seg_tint);
} else {
al_draw_tinted_scaled_bitmap(transition_tiles[t][edge_bits_arr[t]],
seg_tint,
0, 0, (float)tile_w, (float)tile_h,
base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
}
}
int cidx = num_edge_masks + corner_bits_arr[t];
if (corner_bits_arr[t] != 0 && cidx < num_masks &&
transition_tiles[t] && transition_tiles[t][cidx] != NULL) {
if (!blender_is_one && !s_force_alpha) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
blender_is_one = 1;
}
if (map->smooth_height && !tile_is_flat) {
_draw_tile_warped(transition_tiles[t][cidx],
vx, vy, tile_w, tile_h, seg_tint);
} else {
al_draw_tinted_scaled_bitmap(transition_tiles[t][cidx],
seg_tint,
0, 0, (float)tile_w, (float)tile_h,
base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
}
}
}
}
/* Overlay tile (drawn after terrain transitions, before
* walls/borders). Overlay bitmaps are premultiplied (loaded
* through the same path as base tiles + transitions), so
* ALPHA blending would produce quadratic darkening at
* fractional-alpha edges. Use ONE/INVERSE_ALPHA which matches
* the premul source format. Also avoids cross-tile
* contamination under al_hold_bitmap_drawing(true), where
* al_set_blender does not flush the held batch mid-pass: an
* ALPHA blender set here would otherwise apply to draws
* emitted earlier in the batch by other tiles when the batch
* eventually flushes. */
if (overlay_bitmaps && map->overlay && num_overlay_tiles > 0 && !s_no_overlay) {
int ov_idx = my * map->width + mx;
int ov_tile = map->overlay[ov_idx];
if (ov_tile > 0 && ov_tile < num_overlay_tiles && overlay_bitmaps[ov_tile]) {
if (!blender_is_one) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
blender_is_one = 1;
}
if (map->smooth_height && !tile_is_flat) {
_draw_tile_warped(overlay_bitmaps[ov_tile], vx, vy, tile_w, tile_h, seg_tint);
} else {
al_draw_tinted_scaled_bitmap(overlay_bitmaps[ov_tile], seg_tint,
0, 0, (float)tile_w, (float)tile_h,
base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
}
}
}
/* Height borders (ONE blender; lines use RGB=0 so premul == straight) */
if (seg_top > 0 && !s_no_height_borders) {
if (!blender_is_one) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
blender_is_one = 1;
}
_draw_height_borders(map, mx, my, draw_order[di].seg_idx,
0, seg_top,
draw_order[di].underside,
vx, vy, lift);
}
/* Hover highlight (ONE blender; colors premultiplied below) */
if (!s_no_hover_grid && mx == map->hover_mx && my == map->hover_my) {
if (!blender_is_one) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
blender_is_one = 1;
}
/* Premul: each RGB channel scaled by alpha/255. */
ALLEGRO_COLOR hc = player_mode
? al_map_rgba(100 * 120 / 255, 255 * 120 / 255, 100 * 120 / 255, 120)
: al_map_rgba(255 * 120 / 255, 255 * 120 / 255, 100 * 120 / 255, 120);
float verts[8] = {vx[0], vy[0], vx[1], vy[1],
vx[2], vy[2], vx[3], vy[3]};
al_draw_filled_polygon(verts, 4, hc);
ALLEGRO_COLOR oc = player_mode
? al_map_rgba(100 * 200 / 255, 255 * 200 / 255, 100 * 200 / 255, 200)
: al_map_rgba(255 * 200 / 255, 255 * 200 / 255, 100 * 200 / 255, 200);
al_draw_line(vx[0], vy[0], vx[1], vy[1], oc, 1.5f);
al_draw_line(vx[1], vy[1], vx[2], vy[2], oc, 1.5f);
al_draw_line(vx[2], vy[2], vx[3], vy[3], oc, 1.5f);
al_draw_line(vx[3], vy[3], vx[0], vy[0], oc, 1.5f);
}
/* Grid overlay (ONE blender; colors premultiplied below).
* No blender_is_one = 1 update needed: this is the last block in
* the iteration that touches the blender, and the next iteration
* unconditionally re-sets the blender at the top of the loop. */
if (map->show_grid && !s_no_hover_grid) {
if (!blender_is_one) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
}
/* Premul: white 60/255 alpha is (60,60,60,60). */
ALLEGRO_COLOR gc = al_map_rgba(60, 60, 60, 60);
al_draw_line(vx[0], vy[0], vx[1], vy[1], gc, 1.0f);
al_draw_line(vx[1], vy[1], vx[2], vy[2], gc, 1.0f);
al_draw_line(vx[2], vy[2], vx[3], vy[3], gc, 1.0f);
al_draw_line(vx[3], vy[3], vx[0], vy[0], gc, 1.0f);
if (h > 0) {
unsigned char a2 = (unsigned char)(60 + h * 35);
/* Premul: (R*a/255, G*a/255, B*a/255, a) for (255,200,100,a). */
ALLEGRO_COLOR hc2 = al_map_rgba(a2,
(unsigned char)(200 * a2 / 255),
(unsigned char)(100 * a2 / 255),
a2);
float dot_x = (vx[0] + vx[2]) / 2.0f;
float dot_y = (vy[0] + vy[1] + vy[2] + vy[3]) / 4.0f;
al_draw_filled_circle(dot_x, dot_y, 2.0f * zoom, hc2);
}
}
/* Per-tile hold cycle: flush this tile's batch before moving
* on. Each tile becomes its own VBO. */
if (s_hold_per_tile) al_hold_bitmap_drawing(false);
/* Re-enter the held batch for the next batchable tile.
* Resets the blender to ONE for the next tile's base bitmap,
* since immediate-mode primitive draws inside the unbatched
* section may have left other state.
* No blender_is_one = 1 update: the next iteration unconditionally
* re-sets the blender at the top of the loop. */
if (unbatched_tile) {
al_hold_bitmap_drawing(true);
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
}
}
if (effective_hold_pass) al_hold_bitmap_drawing(false);
n_iso_pass1_us += (al_get_time() - _iso_t_pass1) * 1.0e6;
double _iso_t_pass2 = al_get_time();
/* PASS 2: Elevated segments + ALL objects (interleaved)
* Objects are interleaved at every draw_order entry (ground and
* elevated) to preserve correct depth ordering. Only elevated
* segments (bottom > 0) are actually rendered, ground segments
* were already drawn in Pass 1. */
int obj_cursor = 0;
for (int di = 0; di < num_entries; di++) {
int mx = draw_order[di].mx;
int my = draw_order[di].my;
int seg_bottom = draw_order[di].bottom;
/* Object interleaving runs at every entry regardless of
* ground/elevated, this ensures objects at ground-level depth
* positions are drawn at the correct point in the sort order. */
if (nobj > 0) {
int seg_depth = my + mx;
while (obj_cursor < nobj) {
int oi = obj_order[obj_cursor];
int obj_fz = (int)floorf(objects[oi].fz);
int obj_my = (int)floorf(objects[oi].fy);
int obj_mx = (int)floorf(objects[oi].fx);
int obj_depth = obj_my + obj_mx;
if (obj_depth > seg_depth ||
(obj_depth == seg_depth && obj_fz > seg_bottom) ||
(obj_depth == seg_depth && obj_fz == seg_bottom && obj_my > my) ||
(obj_depth == seg_depth && obj_fz == seg_bottom && obj_my == my && obj_mx >= mx))
break;
if (objects[oi].draw) {
objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
zoom, 1.0f, objects[oi].user_data);
}
obj_cursor++;
}
}
/* Ground segments already drawn in Pass 1, skip rendering */
if (seg_bottom == 0) continue;
/* Elevated segment rendering */
int seg_top = draw_order[di].top;
int h = seg_top;
int base = iso_map_get_terrain(map, mx, my);
/* Per-segment tile overrides for elevated segments */
int etop_terrain = base;
int ewall_terrain = base;
{
int si = draw_order[di].seg_idx;
int tidx = my * map->width + mx;
if (map->segments && tidx >= 0 && tidx < map->width * map->height &&
si >= 0 && si < map->segments[tidx].count) {
int ut = map->segments[tidx].segs[si].upper_tile;
int lt = map->segments[tidx].segs[si].lower_tile;
if (ut >= 0 && ut < map->num_terrains) etop_terrain = ut;
if (lt >= 0 && lt < map->num_terrains) ewall_terrain = lt;
}
}
float vx[4], vy[4];
float base_dx, base_dy;
_iso_corner_to_screen(hw0, hh0, tl0, mx, my, (float)h, cam_px, cam_py, zoom, &vx[0], &vy[0]);
_iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &vx[1], &vy[1]);
_iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &vx[2], &vy[2]);
_iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &vx[3], &vy[3]);
base_dx = vx[3];
base_dy = vy[0];
/* Culling, objects already handled above, safe to continue */
float min_vy = fminf(fminf(vy[0], vy[1]), fminf(vy[2], vy[3]));
float max_vy = fmaxf(fmaxf(vy[0], vy[1]), fmaxf(vy[2], vy[3]));
float max_side = fmaxf((float)h, (float)map->max_height) * lift;
if (vx[1] < 0 || vx[3] > (float)screen_w) continue;
if (max_vy + max_side < 0 || min_vy > (float)screen_h) continue;
/* Underside face for floating segments */
if (draw_order[di].underside && tile_bitmaps[etop_terrain]) {
float under_sx, under_sy;
iso_map_to_screen(map, mx, my, seg_bottom, &under_sx, &under_sy);
float under_dx = under_sx * zoom + cam_px;
float under_dy = under_sy * zoom + cam_py;
_draw_segment_underside(tile_bitmaps[etop_terrain], under_dx, under_dy,
phw, phh, tile_draw_w, tile_draw_h,
tile_w, tile_h);
}
/* Height-based tint for elevated segment */
/* Elevated segment: same ambient + dynamic light from base tile */
float eamb_r = map->ambient_r > 0.0f ? map->ambient_r : 1.0f;
float eamb_g = map->ambient_g > 0.0f ? map->ambient_g : 1.0f;
float eamb_b = map->ambient_b > 0.0f ? map->ambient_b : 1.0f;
float edyn_r = 0.0f, edyn_g = 0.0f, edyn_b = 0.0f;
if (map->dynamic_light_map) {
int elidx = (my * map->width + mx) * 3;
edyn_r = map->dynamic_light_map[elidx];
edyn_g = map->dynamic_light_map[elidx + 1];
edyn_b = map->dynamic_light_map[elidx + 2];
}
ALLEGRO_COLOR eseg_tint = _compute_tile_tint(seg_top, map->max_height,
map->height_tint_intensity,
eamb_r, eamb_g, eamb_b,
edyn_r, edyn_g, edyn_b);
float eseg_brightness = 1.0f;
if (map->height_tint_intensity > 0.0f && map->max_height > 0) {
float ratio = (float)seg_top / (float)map->max_height;
eseg_brightness = 1.0f - map->height_tint_intensity * (1.0f - ratio);
}
eseg_brightness *= (eamb_r + eamb_g + eamb_b) / 3.0f;
eseg_brightness += (edyn_r + edyn_g + edyn_b) / 3.0f;
if (eseg_brightness > 1.0f) eseg_brightness = 1.0f;
/* Top surface (flat, elevated segments don't use smooth height) */
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
al_draw_tinted_scaled_bitmap(tile_bitmaps[etop_terrain], eseg_tint,
0, 0, (float)tile_w, (float)tile_h,
base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
/* Cliff walls + height borders */
if (seg_top > seg_bottom) {
_draw_segment_sides(map, vx[1], vy[1], vx[2], vy[2], vx[3], vy[3],
mx, my, seg_bottom, seg_top, lift, eseg_brightness, ewall_terrain);
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
_draw_height_borders(map, mx, my, draw_order[di].seg_idx,
seg_bottom, seg_top,
draw_order[di].underside,
vx, vy, lift);
}
/* Hover highlight */
if (mx == map->hover_mx && my == map->hover_my) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
ALLEGRO_COLOR hc = player_mode
? al_map_rgba(100, 255, 100, 120)
: al_map_rgba(255, 255, 100, 120);
float verts[8] = {vx[0], vy[0], vx[1], vy[1],
vx[2], vy[2], vx[3], vy[3]};
al_draw_filled_polygon(verts, 4, hc);
ALLEGRO_COLOR oc = player_mode
? al_map_rgba(100, 255, 100, 200)
: al_map_rgba(255, 255, 100, 200);
al_draw_line(vx[0], vy[0], vx[1], vy[1], oc, 1.5f);
al_draw_line(vx[1], vy[1], vx[2], vy[2], oc, 1.5f);
al_draw_line(vx[2], vy[2], vx[3], vy[3], oc, 1.5f);
al_draw_line(vx[3], vy[3], vx[0], vy[0], oc, 1.5f);
}
/* Grid overlay */
if (map->show_grid) {
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
ALLEGRO_COLOR gc = al_map_rgba(255, 255, 255, 60);
al_draw_line(vx[0], vy[0], vx[1], vy[1], gc, 1.0f);
al_draw_line(vx[1], vy[1], vx[2], vy[2], gc, 1.0f);
al_draw_line(vx[2], vy[2], vx[3], vy[3], gc, 1.0f);
al_draw_line(vx[3], vy[3], vx[0], vy[0], gc, 1.0f);
if (h > 0) {
ALLEGRO_COLOR hc2 = al_map_rgba(255, 200, 100,
(unsigned char)(60 + h * 35));
float dot_x = (vx[0] + vx[2]) / 2.0f;
float dot_y = (vy[0] + vy[1] + vy[2] + vy[3]) / 4.0f;
al_draw_filled_circle(dot_x, dot_y, 2.0f * zoom, hc2);
}
}
}
/* Draw any remaining objects past the last entry */
while (obj_cursor < nobj) {
int oi = obj_order[obj_cursor];
if (objects[oi].draw) {
objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
zoom, 1.0f, objects[oi].user_data);
}
obj_cursor++;
}
n_iso_pass2_us += (al_get_time() - _iso_t_pass2) * 1.0e6;
double _iso_t_pass3 = al_get_time();
/* Step 7: Occlusion detection and clipped overlay pass.
* For each occluded entity, restrict the ghost draw to the region
* below the occluding tile's north vertex using a clipping rectangle. */
for (int i = 0; i < nobj; i++) {
int oi = obj_order[i];
if (objects[oi].occluded_alpha <= 0.0f) continue;
if (!objects[oi].draw) continue;
float clip_y = 1e9f;
if (!_iso_object_check_occlusion(map,
objects[oi].fx, objects[oi].fy, objects[oi].fz,
obj_sx_arr[oi], obj_sy_arr[oi],
objects[oi].sprite_h,
objects[oi].sprite_half_w,
cam_px, cam_py, zoom,
&clip_y))
continue;
objects[oi].is_occluded = 1;
objects[oi].occlude_clip_y = clip_y;
/* Clip ghost overlay to below the occluder's top edge. The
* ghost is drawn at alpha < 1 to make the occluded sprite
* look semi-transparent. Use straight-alpha blending here:
* with the premultiplied (ADD/ONE/INV_ALPHA) blender the
* 0.4-alpha overlay is ADDED on top of the already-drawn
* full-bright sprite, brightening it instead of revealing
* what's behind. ALPHA/INV_ALPHA does the standard
* src*alpha + dst*(1-alpha) transparent compositing the
* caller meant. */
int prev_cx, prev_cy, prev_cw, prev_ch;
al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
int clip_top = (int)floorf(clip_y);
if (clip_top < prev_cy) clip_top = prev_cy;
al_set_clipping_rectangle(prev_cx, clip_top,
prev_cw, prev_cy + prev_ch - clip_top);
al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
zoom, objects[oi].occluded_alpha, objects[oi].user_data);
al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
}
n_iso_pass3_us += (al_get_time() - _iso_t_pass3) * 1.0e6;
/* The draw_order / obj_* buffers are file-static and reused
* across calls; no per-call free. They survive for the process
* lifetime which is fine: max draw entries is bounded by
* map->width * map->height * ISO_MAX_SEGMENTS_PER_TILE (~2 KB
* for typical 32x32x2 chunks), and obj_buf_cap grows once to the
* scene's peak entity count. */
} /* iso_map_draw() */
/* Transition mask & tile generation (Article 934) */
void iso_mask_tile_to_diamond(ALLEGRO_BITMAP* bmp, int tile_w, int tile_h) {
__n_assert(bmp, return);
ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(bmp,
ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_READWRITE);
if (!lr) return;
for (int py = 0; py < tile_h; py++) {
unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
for (int px = 0; px < tile_w; px++) {
if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
row[px * 4 + 0] = 0;
row[px * 4 + 1] = 0;
row[px * 4 + 2] = 0;
row[px * 4 + 3] = 0;
}
}
}
al_unlock_bitmap(bmp);
}
void iso_generate_transition_masks(ALLEGRO_BITMAP** masks, int tile_w, int tile_h) {
__n_assert(masks, return);
float cx = (float)tile_w / 2.0f;
float cy = (float)tile_h / 2.0f;
ALLEGRO_STATE state;
al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
/* Edge masks (0..15) */
for (int mask = 0; mask < ISO_NUM_EDGE_MASKS; mask++) {
masks[mask] = al_create_bitmap(tile_w, tile_h);
al_set_target_bitmap(masks[mask]);
al_clear_to_color(al_map_rgba(0, 0, 0, 0));
ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(masks[mask],
ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_WRITEONLY);
if (!lr) continue;
for (int py = 0; py < tile_h; py++) {
unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
for (int px = 0; px < tile_w; px++) {
if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
row[px * 4 + 0] = 0;
row[px * 4 + 1] = 0;
row[px * 4 + 2] = 0;
row[px * 4 + 3] = 0;
continue;
}
float alpha = 0.0f;
float nx = ((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
float ny = ((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
float gw = 0.65f;
if (mask & ISO_EDGE_W) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (-nx - ny) / gw)));
if (mask & ISO_EDGE_N) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (nx - ny) / gw)));
if (mask & ISO_EDGE_E) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (nx + ny) / gw)));
if (mask & ISO_EDGE_S) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (-nx + ny) / gw)));
if (alpha > 1.0f) alpha = 1.0f;
alpha = alpha * alpha * (3.0f - 2.0f * alpha);
unsigned char a = (unsigned char)(alpha * 255.0f);
row[px * 4 + 0] = a;
row[px * 4 + 1] = a;
row[px * 4 + 2] = a;
row[px * 4 + 3] = a;
}
}
al_unlock_bitmap(masks[mask]);
}
/* Corner masks (16..31)
* In isometric diamond projection, diagonal neighbors map to diamond tips:
* NW (mx-1,my-1) = North tip (0, -1) in normalized coords
* NE (mx+1,my-1) = East tip (1, 0)
* SE (mx+1,my+1) = South tip (0, 1)
* SW (mx-1,my+1) = West tip (-1, 0) */
for (int mask = 0; mask < ISO_NUM_CORNER_MASKS; mask++) {
int idx = ISO_NUM_EDGE_MASKS + mask;
masks[idx] = al_create_bitmap(tile_w, tile_h);
al_set_target_bitmap(masks[idx]);
al_clear_to_color(al_map_rgba(0, 0, 0, 0));
ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(masks[idx],
ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_WRITEONLY);
if (!lr) continue;
for (int py = 0; py < tile_h; py++) {
unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
for (int px = 0; px < tile_w; px++) {
if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
row[px * 4 + 0] = 0;
row[px * 4 + 1] = 0;
row[px * 4 + 2] = 0;
row[px * 4 + 3] = 0;
continue;
}
float alpha = 0.0f;
float nx = ((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
float ny = ((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
float corner_radius = 0.55f;
if (mask & ISO_CORNER_NW) {
float dy = ny + 1.0f;
float dist = sqrtf(nx * nx + dy * dy);
float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
alpha = fmaxf(alpha, c_alpha);
}
if (mask & ISO_CORNER_NE) {
float dx = nx - 1.0f;
float dist = sqrtf(dx * dx + ny * ny);
float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
alpha = fmaxf(alpha, c_alpha);
}
if (mask & ISO_CORNER_SE) {
float dy = ny - 1.0f;
float dist = sqrtf(nx * nx + dy * dy);
float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
alpha = fmaxf(alpha, c_alpha);
}
if (mask & ISO_CORNER_SW) {
float dx = nx + 1.0f;
float dist = sqrtf(dx * dx + ny * ny);
float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
alpha = fmaxf(alpha, c_alpha);
}
if (alpha > 1.0f) alpha = 1.0f;
alpha = alpha * alpha * (3.0f - 2.0f * alpha);
unsigned char a = (unsigned char)(alpha * 255.0f);
row[px * 4 + 0] = a;
row[px * 4 + 1] = a;
row[px * 4 + 2] = a;
row[px * 4 + 3] = a;
}
}
al_unlock_bitmap(masks[idx]);
}
al_restore_state(&state);
n_log(LOG_INFO, "Generated %d transition masks (%dx%d)", ISO_NUM_MASKS, tile_w, tile_h);
}
void iso_generate_transition_tiles(ALLEGRO_BITMAP*** tiles,
ALLEGRO_BITMAP** masks,
ALLEGRO_BITMAP** tile_bitmaps,
int num_terrains,
int tile_w,
int tile_h) {
__n_assert(tiles, return);
__n_assert(masks, return);
__n_assert(tile_bitmaps, return);
ALLEGRO_STATE state;
al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
for (int t = 0; t < num_terrains; t++) {
for (int m = 0; m < ISO_NUM_MASKS; m++) {
/* Skip mask 0 (no edges active) and mask 16 (no corners active) */
if ((m < ISO_NUM_EDGE_MASKS && m == 0) ||
(m >= ISO_NUM_EDGE_MASKS && m == ISO_NUM_EDGE_MASKS)) {
tiles[t][m] = NULL;
continue;
}
tiles[t][m] = al_create_bitmap(tile_w, tile_h);
al_set_target_bitmap(tiles[t][m]);
al_clear_to_color(al_map_rgba(0, 0, 0, 0));
/* Draw the terrain tile first */
al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
al_draw_bitmap(tile_bitmaps[t], 0, 0, 0);
/* Multiply by the alpha mask: keeps terrain color
* but applies the mask's alpha for the transition gradient */
al_set_blender(ALLEGRO_ADD, ALLEGRO_DEST_COLOR, ALLEGRO_ZERO);
al_draw_bitmap(masks[m], 0, 0, 0);
}
}
al_restore_state(&state);
n_log(LOG_INFO, "Pre-composited transition tiles for %d terrains x %d masks", num_terrains, ISO_NUM_MASKS);
}
#endif /* HAVE_ALLEGRO - iso_map_draw */
#ifdef N_ASTAR_H
ASTAR_GRID* iso_map_to_astar_grid(const ISO_MAP* map, int max_height_diff, int start_x, int start_y) {
__n_assert(map, return NULL);
ASTAR_GRID* grid = n_astar_grid_new(map->width, map->height, 1);
if (!grid) return NULL;
int total = map->width * map->height;
/* First pass: all cells unwalkable, set costs */
for (int y = 0; y < map->height; y++) {
for (int x = 0; x < map->width; x++) {
n_astar_grid_set_walkable(grid, x, y, 0, 0);
int h = map->heightmap[y * map->width + x];
int cost = ASTAR_COST_CARDINAL + h * 100;
n_astar_grid_set_cost(grid, x, y, 0, cost);
}
}
/* Flood-fill from (start_x, start_y) to mark reachable cells as walkable.
* This ensures height constraints are checked per-edge (between consecutive
* cells) rather than per-cell, so tiles on a plateau edge remain walkable
* when approached from the same elevation. */
if (start_x < 0 || start_x >= map->width || start_y < 0 || start_y >= map->height)
return grid;
int start_ab = map->ability[start_y * map->width + start_x];
if (start_ab != WALK && start_ab != SWIM)
return grid;
int* queue = (int*)malloc((size_t)total * 2 * sizeof(int));
if (!queue) {
n_astar_grid_free(grid);
return NULL;
}
uint8_t* visited = (uint8_t*)calloc((size_t)total, sizeof(uint8_t));
if (!visited) {
free(queue);
n_astar_grid_free(grid);
return NULL;
}
int qfront = 0, qback = 0;
/* seed the start cell */
visited[start_y * map->width + start_x] = 1;
n_astar_grid_set_walkable(grid, start_x, start_y, 0, 1);
queue[qback++] = start_x;
queue[qback++] = start_y;
/* 8-directional expansion (matches ASTAR_ALLOW_DIAGONAL) */
const int dirs[][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}};
while (qfront < qback) {
int cx = queue[qfront++];
int cy = queue[qfront++];
int ch = map->heightmap[cy * map->width + cx];
for (int d = 0; d < 8; d++) {
int nx = cx + dirs[d][0];
int ny = cy + dirs[d][1];
if (nx < 0 || nx >= map->width || ny < 0 || ny >= map->height) continue;
if (visited[ny * map->width + nx]) continue;
int nab = map->ability[ny * map->width + nx];
if (nab != WALK && nab != SWIM) continue;
int nh = map->heightmap[ny * map->width + nx];
if (max_height_diff >= 0 && abs(ch - nh) > max_height_diff) continue;
visited[ny * map->width + nx] = 1;
n_astar_grid_set_walkable(grid, nx, ny, 0, 1);
queue[qback++] = nx;
queue[qback++] = ny;
}
}
free(queue);
free(visited);
return grid;
}
#endif /* N_ASTAR_H */
#endif /* #ifndef NOISOENGINE */
Modules
- AABB3D
- ACCEPT_POOL
- ALLEGRO4
- ALLEGRO5
- ANIM_LIB
- ASTAR
- ATLAS_LIB
- AVRO
- CLOCK_SYNC
- COMMONS
- CONFIGFILE
- CYPHER_BASE64
- CYPHER_VIGENERE
- DEAD_RECKONING
- ENUMS
- EXCEPTIONS
- GAME_ENV
- GIT
- GUI
- HASH_TABLE
- ISOMETRIC_ENGINE
- LIST
- LOG
- LOGNODUP
- LZ4
- NETWORKING
- NETWORK_MSG
- N_COOKIES
- N_DIFF
- N_DIGEST
- N_DISK_CACHE
- N_DNS
- N_ECC
- N_ENTROPY
- N_FILES
- N_FLUIDS
- N_GUI_COLOR
- N_GUI_COLORWHEEL
- N_GUI_CURVE
- N_GUI_GRID
- N_GUI_POPUP
- N_HEX
- N_HTTP3
- N_HTTP_COOKIE
- N_HTTP_FORM
- N_HTTP_MIME
- N_HTTP_REQ
- N_HTTP_RESP
- N_HTTP_ROUTER
- N_HTTP_SERVER
- N_ICC
- N_JSON
- N_KAFKA
- N_LINALG
- N_MODULE
- N_OPENSSL_COMPAT
- N_PRETTY
- N_PTH
- N_QUERY
- N_RANDOM
- N_SHA256
- N_STR
- N_TIFF
- N_TIME
- N_USER
- N_X509
- N_ZIP
- PARTICLES
- PCRE
- PHYSICS
- SIGNALS
- STACK
- THREADS
- TRAJECTORY
- TREE
- ZLIB