Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
n_iso_engine.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
28
29#include <math.h>
30#include <stdio.h>
31#include <stdlib.h> /* realloc, free, getenv */
32
33/* Include n_astar.h BEFORE n_iso_engine.h so the iso_map_to_astar_grid
34 * function is enabled via the N_ASTAR_H guard */
35#include "nilorea/n_astar.h"
37
38#ifndef __windows__
39#include <fcntl.h>
40#include <unistd.h>
41
42/* Open a file for writing with explicit owner-only write permission (0644)
43 * instead of the world-writable 0666 that fopen() requests before the umask
44 * is applied. On Windows the POSIX mode bits do not apply, so fopen() is used
45 * directly. */
46static FILE* _iso_fopen_write(const char* path, const char* mode) {
47 int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644);
48 if (fd < 0)
49 return NULL;
50 FILE* f = fdopen(fd, mode);
51 if (!f)
52 close(fd);
53 return f;
54}
55#else
56#define _iso_fopen_write(path, mode) fopen((path), (mode))
57#endif
58
59/* Per-pass timing accumulators. Host resets these at frame start,
60 * iso_map_draw adds elapsed microseconds per pass per invocation. See
61 * the doxygen block in n_iso_engine.h for usage. */
62double n_iso_pass1_us = 0.0;
63double n_iso_pass2_us = 0.0;
64double n_iso_pass3_us = 0.0;
65double n_iso_setup_us = 0.0;
66
67#ifdef HAVE_ALLEGRO
68/* Optional primitive-color filter (sRGB-aware consumers register a
69 * design->linear converter here). See header docstring. NULL = identity.
70 * Gated on HAVE_ALLEGRO because the prototype mentions ALLEGRO_COLOR
71 * which doesn't exist in the no-allegro test build path. */
73
77
78ALLEGRO_COLOR iso_apply_primitive_color_filter(ALLEGRO_COLOR c) {
80 return c;
81}
82
83/* Line-jitter compensation callback. See header doc. */
85
89
90void iso_apply_line_jitter_compensation(float* out_dx, float* out_dy) {
92 g_iso_line_jitter_fn(out_dx, out_dy);
93 } else {
94 if (out_dx) *out_dx = 0.0f;
95 if (out_dy) *out_dy = 0.0f;
96 }
97}
98#endif /* HAVE_ALLEGRO */
99
100#ifndef NOISOENGINE
101
102/* Height-aware ISO_MAP API (Articles 747/748/934/1269/2026)
103 * Core API does not require Allegro and compiles everywhere.
104 */
105
114ISO_MAP* iso_map_new(int width, int height, int num_terrains, int max_height) {
115 __n_assert(width > 0, return NULL);
116 __n_assert(height > 0, return NULL);
117
118 ISO_MAP* map = NULL;
119 Malloc(map, ISO_MAP, 1);
120 __n_assert(map, return NULL);
121
122 map->width = width;
123 map->height = height;
124 map->num_terrains = num_terrains;
125 map->max_height = max_height;
126
127 size_t total = (size_t)width * (size_t)height;
128
129 Malloc(map->terrain, int, total);
130 if (!map->terrain) {
131 Free(map);
132 return NULL;
133 }
134
135 Malloc(map->heightmap, int, total);
136 if (!map->heightmap) {
137 Free(map->terrain);
138 Free(map);
139 return NULL;
140 }
141
142 Malloc(map->ability, int, total);
143 if (!map->ability) {
144 Free(map->heightmap);
145 Free(map->terrain);
146 Free(map);
147 return NULL;
148 }
149
150 memset(map->terrain, 0, total * sizeof(int));
151 memset(map->heightmap, 0, total * sizeof(int));
152 for (size_t i = 0; i < total; i++) {
153 map->ability[i] = WALK;
154 }
155
156 /* default classic 2:1 projection for 64px wide tiles */
158
159 map->segments = NULL; /* allocated on demand by iso_map_set_segments() */
160
161 /* Overlay layer: allocated and zeroed, 0 = no overlay */
162 Malloc(map->overlay, int, total);
163 if (map->overlay) memset(map->overlay, 0, total * sizeof(int));
164
165 /* default rendering flags */
166 map->smooth_height = 0;
167 map->smooth_slope_max = 1;
168 map->show_grid = 0;
169 map->hover_mx = -1;
170 map->hover_my = -1;
171 map->height_tint_intensity = 0.0f; /* disabled by default; game sets this */
172
173 /* Ambient color defaults: full daylight (no tinting) */
174 map->ambient_r = 1.0f;
175 map->ambient_g = 1.0f;
176 map->ambient_b = 1.0f;
177 map->dynamic_light_map = NULL; /* caller-owned, set before draw */
178
179 /* Draw_order cache. dirty=1 forces a first build. */
180 map->cached_draw_order = NULL;
182 map->cached_draw_order_cap = 0;
183 map->draw_order_dirty = 1;
184
185 return map;
186} /* iso_map_new() */
187
192void iso_map_free(ISO_MAP** map_ptr) {
193 __n_assert(map_ptr, return);
194 __n_assert(*map_ptr, return);
195 ISO_MAP* map = *map_ptr;
196 Free(map->terrain);
197 Free(map->heightmap);
198 Free(map->ability);
199 Free(map->segments);
200 Free(map->overlay);
201 /* Cached draw_order */
202 if (map->cached_draw_order) {
203 free(map->cached_draw_order);
204 map->cached_draw_order = NULL;
205 }
206 Free(*map_ptr);
207} /* iso_map_free() */
208
214static float _iso_preset_angle(int preset) {
215 switch (preset) {
217 return 30.0f;
219 return 18.43f;
221 return 45.0f;
222 case ISO_PROJ_CLASSIC:
223 default:
224 return 26.565f;
225 }
226}
227
234void iso_map_set_projection(ISO_MAP* map, int preset, float tile_width) {
235 __n_assert(map, return);
236 float hw = tile_width / 2.0f;
237 float angle = _iso_preset_angle(preset);
238 float hh = hw * tanf(angle * (float)M_PI / 180.0f);
239
240 map->proj.half_w = hw;
241 map->proj.half_h = hh;
242 map->proj.angle_deg = angle;
243 map->proj.target_angle = angle;
244 map->proj.lerp_speed = 3.0f;
245 map->proj.tile_lift = hh;
246} /* iso_map_set_projection() */
247
255 __n_assert(map, return);
256 map->proj.target_angle = _iso_preset_angle(preset);
257} /* iso_map_set_projection_target() */
258
265void iso_map_lerp_projection(ISO_MAP* map, float dt) {
266 __n_assert(map, return);
267 float diff = map->proj.target_angle - map->proj.angle_deg;
268 if (fabsf(diff) < 0.01f) {
269 map->proj.angle_deg = map->proj.target_angle;
270 } else {
271 map->proj.angle_deg += diff * map->proj.lerp_speed * dt;
272 }
273 float rad = map->proj.angle_deg * (float)M_PI / 180.0f;
274 map->proj.half_h = map->proj.half_w * sinf(rad) / cosf(rad);
275 if (map->proj.half_h < 8.0f) map->proj.half_h = 8.0f;
276 if (map->proj.half_h > map->proj.half_w * 2.0f) map->proj.half_h = map->proj.half_w * 2.0f;
277} /* iso_map_lerp_projection() */
278
284const char* iso_projection_name(int preset) {
285 switch (preset) {
286 case ISO_PROJ_CLASSIC:
287 return "Classic 2:1";
289 return "True Isometric";
291 return "Staggered";
293 return "Military";
294 default:
295 return "Unknown";
296 }
297} /* iso_projection_name() */
298
306void iso_map_set_projection_custom(ISO_MAP* map, float half_w, float half_h, float tile_lift) {
307 __n_assert(map, return);
308 map->proj.half_w = half_w;
309 map->proj.half_h = half_h;
310 map->proj.tile_lift = tile_lift;
311 map->proj.angle_deg = atanf(half_h / half_w) * 180.0f / (float)M_PI;
312 map->proj.target_angle = map->proj.angle_deg;
313} /* iso_map_set_projection_custom() */
314
322int iso_map_get_terrain(const ISO_MAP* map, int mx, int my) {
323 __n_assert(map, return 0);
324 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
325 return map->terrain[my * map->width + mx];
326} /* iso_map_get_terrain() */
327
335void iso_map_set_terrain(ISO_MAP* map, int mx, int my, int terrain) {
336 __n_assert(map, return);
337 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
338 map->terrain[my * map->width + mx] = terrain;
339} /* iso_map_set_terrain() */
340
348int iso_map_get_height(const ISO_MAP* map, int mx, int my) {
349 __n_assert(map, return 0);
350 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
351 return map->heightmap[my * map->width + mx];
352} /* iso_map_get_height() */
353
361void iso_map_set_height(ISO_MAP* map, int mx, int my, int h) {
362 __n_assert(map, return);
363 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
364 if (h < 0) h = 0;
365 if (h > map->max_height) h = map->max_height;
366 map->heightmap[my * map->width + mx] = h;
367} /* iso_map_set_height() */
368
376int iso_map_get_ability(const ISO_MAP* map, int mx, int my) {
377 __n_assert(map, return 0);
378 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
379 return map->ability[my * map->width + mx];
380} /* iso_map_get_ability() */
381
389void iso_map_set_ability(ISO_MAP* map, int mx, int my, int ab) {
390 __n_assert(map, return);
391 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
392 map->ability[my * map->width + mx] = ab;
393} /* iso_map_set_ability() */
394
407void iso_map_to_screen(const ISO_MAP* map, int mx, int my, int h, float* screen_x, float* screen_y) {
408 __n_assert(map, return);
409 __n_assert(screen_x, return);
410 __n_assert(screen_y, return);
411 *screen_x = ((float)mx - (float)my) * map->proj.half_w + map->proj.half_w;
412 *screen_y = ((float)mx + (float)my) * map->proj.half_h - (float)h * map->proj.tile_lift;
413} /* iso_map_to_screen() */
414
425void iso_screen_to_map(const ISO_MAP* map, float screen_x, float screen_y, int* mx, int* my) {
426 __n_assert(map, return);
427 __n_assert(mx, return);
428 __n_assert(my, return);
429 float tw = 2.0f * map->proj.half_w;
430 float th = 2.0f * map->proj.half_h;
431 *mx = (int)floorf(screen_x / tw + screen_y / th - 1.0f);
432 *my = (int)floorf(screen_y / th - screen_x / tw + 1.0f);
433} /* iso_screen_to_map() */
434
448void iso_map_to_screen_f(const ISO_MAP* map, float fmx, float fmy, float h, float* screen_x, float* screen_y) {
449 __n_assert(map, return);
450 __n_assert(screen_x, return);
451 __n_assert(screen_y, return);
452 *screen_x = (fmx - fmy) * map->proj.half_w + 2.0f * map->proj.half_w;
453 *screen_y = (fmx + fmy) * map->proj.half_h - h * map->proj.tile_lift;
454} /* iso_map_to_screen_f() */
455
476void 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) {
477 __n_assert(map, return);
478 __n_assert(sx, return);
479 __n_assert(sy, return);
480 float hw = map->proj.half_w;
481 float hh = map->proj.half_h;
482 float tl = map->proj.tile_lift;
483 float wx = (float)(cx - cy) * hw + 2.0f * hw;
484 float wy = (float)(cx + cy) * hh - fh * tl;
485 *sx = wx * zoom + cam_px;
486 *sy = wy * zoom + cam_py;
487} /* iso_corner_to_screen() */
488
498int iso_is_in_diamond(int px, int py, int tile_w, int tile_h) {
499 float cx = (float)tile_w / 2.0f;
500 float cy = (float)tile_h / 2.0f;
501 float dx = fabsf((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
502 float dy = fabsf((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
503 return (dx + dy) <= 1.0f;
504} /* iso_is_in_diamond() */
505
516float iso_diamond_dist(int px, int py, int tile_w, int tile_h) {
517 float cx = (float)tile_w / 2.0f;
518 float cy = (float)tile_h / 2.0f;
519 float dx = fabsf((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
520 float dy = fabsf((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
521 float d = dx + dy;
522 if (d > 1.0f) return 0.0f;
523 return 1.0f - d;
524} /* iso_diamond_dist() */
525
539void 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) {
540 __n_assert(map, return);
541 __n_assert(mx, return);
542 __n_assert(my, return);
543
544 float half_w = map->proj.half_w;
545 float half_h = map->proj.half_h;
546 float tile_lift = map->proj.tile_lift;
547 float tw = half_w * 2.0f;
548 float th = half_h * 2.0f;
549
550 for (int h = map->max_height; h >= 0; h--) {
551 float adj_sy = screen_y + (float)h * tile_lift;
552 float fmx = screen_x / tw + adj_sy / th - 1.0f;
553 float fmy = adj_sy / th - screen_x / tw + 1.0f;
554 int tmx = (int)floorf(fmx);
555 int tmy = (int)floorf(fmy);
556
557 if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height)
558 continue;
559
560 if (iso_map_get_height(map, tmx, tmy) == h) {
561 float tile_sx, tile_sy;
562 iso_map_to_screen(map, tmx, tmy, h, &tile_sx, &tile_sy);
563 float norm_x = (screen_x - tile_sx) / tw * (float)tile_w;
564 float norm_y = (screen_y - tile_sy) / th * (float)tile_h;
565 int lpx = (int)norm_x;
566 int lpy = (int)norm_y;
567 if (lpx >= 0 && lpx < tile_w && lpy >= 0 && lpy < tile_h &&
568 iso_is_in_diamond(lpx, lpy, tile_w, tile_h)) {
569 *mx = tmx;
570 *my = tmy;
571 return;
572 }
573 }
574 }
575
576 /* Fallback: flat projection (height 0) */
577 *mx = (int)floorf(screen_x / tw + screen_y / th - 1.0f);
578 *my = (int)floorf(screen_y / th - screen_x / tw + 1.0f);
579} /* iso_screen_to_map_height() */
580
595void 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) {
596 __n_assert(map, return);
597 __n_assert(mx, return);
598 __n_assert(my, return);
599
600 float half_w = map->proj.half_w;
601 float half_h = map->proj.half_h;
602 float tile_lift = map->proj.tile_lift;
603 float tw = half_w * 2.0f;
604 float th = half_h * 2.0f;
605
606 for (int h = map->max_height; h >= 0; h--) {
607 float adj_sy = screen_y + (float)h * tile_lift;
608 float fmx = screen_x / tw + adj_sy / th - 1.0f;
609 float fmy = adj_sy / th - screen_x / tw + 1.0f;
610 int tmx = (int)floorf(fmx);
611 int tmy = (int)floorf(fmy);
612
613 if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height)
614 continue;
615
616 if (iso_map_get_height(map, tmx, tmy) == h) {
617 float tile_sx, tile_sy;
618 iso_map_to_screen(map, tmx, tmy, h, &tile_sx, &tile_sy);
619 float norm_x = (screen_x - tile_sx) / tw * (float)tile_w;
620 float norm_y = (screen_y - tile_sy) / th * (float)tile_h;
621 int lpx = (int)norm_x;
622 int lpy = (int)norm_y;
623 if (lpx >= 0 && lpx < tile_w && lpy >= 0 && lpy < tile_h &&
624 iso_is_in_diamond(lpx, lpy, tile_w, tile_h)) {
625 *mx = tmx;
626 *my = tmy;
627 if (out_fx || out_fy) {
628 /* Refine fractional coordinates using interpolated height
629 * so that iso_map_to_screen_f(fx, fy, interp_h) returns
630 * the original screen position. Without this, sub-tile
631 * positions are off when the bilinear-interpolated height
632 * differs from the tile's integer height (e.g. at edges
633 * between tiles of different heights).
634 *
635 * Only refine in SMOOTH mode. In CUT mode the rendered
636 * tile sits at the flat integer height, so the initial
637 * fmx/fmy (computed at that height) are already correct.
638 * Refining with bilinear-interpolated heights in CUT mode
639 * pulls the position toward tile corners and causes
640 * click-to-move to land at the wrong spot. */
641 float refined_fx = fmx;
642 float refined_fy = fmy;
643 if (map->smooth_height) {
644 /* Upper bounds: allow the full extent of the last tile.
645 * Tile tmx occupies [tmx, tmx+1) in map coords, and the
646 * last valid position must stay < map->width so that
647 * floor() never yields an out-of-bounds tile index. */
648 float map_max_fx = (float)map->width - 1e-4f;
649 float map_max_fy = (float)map->height - 1e-4f;
650 /* Tile-local bounds to prevent divergence: clamp each
651 * refinement step to the detected tile so that large
652 * height differences with neighbours cannot pull the
653 * position into an adjacent tile. */
654 float tile_min_fx = (float)tmx;
655 float tile_min_fy = (float)tmy;
656 float tile_max_fx = (float)(tmx + 1);
657 float tile_max_fy = (float)(tmy + 1);
658 if (tile_max_fx > map_max_fx) tile_max_fx = map_max_fx;
659 if (tile_max_fy > map_max_fy) tile_max_fy = map_max_fy;
660 for (int iter = 0; iter < 3; iter++) {
661 float interp_h = iso_map_interpolate_height(map, refined_fx, refined_fy);
662 float new_adj_sy = screen_y + interp_h * tile_lift;
663 refined_fx = screen_x / tw + new_adj_sy / th - 1.0f;
664 refined_fy = new_adj_sy / th - screen_x / tw + 1.0f;
665 /* Keep within detected tile */
666 if (refined_fx < tile_min_fx) refined_fx = tile_min_fx;
667 if (refined_fy < tile_min_fy) refined_fy = tile_min_fy;
668 if (refined_fx > tile_max_fx) refined_fx = tile_max_fx;
669 if (refined_fy > tile_max_fy) refined_fy = tile_max_fy;
670 }
671 }
672 /* Clamp to map bounds so border clicks stay valid */
673 float map_max_fx2 = (float)map->width - 1e-4f;
674 float map_max_fy2 = (float)map->height - 1e-4f;
675 if (refined_fx < 0.0f) refined_fx = 0.0f;
676 if (refined_fy < 0.0f) refined_fy = 0.0f;
677 if (refined_fx > map_max_fx2) refined_fx = map_max_fx2;
678 if (refined_fy > map_max_fy2) refined_fy = map_max_fy2;
679 if (out_fx) *out_fx = refined_fx;
680 if (out_fy) *out_fy = refined_fy;
681 }
682 return;
683 }
684 }
685 }
686
687 /* Fallback: flat projection (height 0) */
688 float fmx = screen_x / tw + screen_y / th - 1.0f;
689 float fmy = screen_y / th - screen_x / tw + 1.0f;
690 *mx = (int)floorf(fmx);
691 *my = (int)floorf(fmy);
692 if (out_fx) *out_fx = fmx;
693 if (out_fy) *out_fy = fmy;
694} /* iso_screen_to_map_height_f() */
695
704float iso_map_interpolate_height(const ISO_MAP* map, float fx, float fy) {
705 __n_assert(map, return 0.0f);
706
707 /* Clamp to valid map range so border tiles interpolate correctly.
708 * Allow the full extent of the last tile (up to width/height minus
709 * a tiny epsilon) so that positions in the right half of border
710 * tiles are not snapped to the tile corner. */
711 if (fx < 0.0f) fx = 0.0f;
712 if (fy < 0.0f) fy = 0.0f;
713 float max_fx = (float)map->width - 1e-4f;
714 float max_fy = (float)map->height - 1e-4f;
715 if (fx > max_fx) fx = max_fx;
716 if (fy > max_fy) fy = max_fy;
717
718 int ix = (int)floorf(fx);
719 int iy = (int)floorf(fy);
720 float frac_x = fx - (float)ix;
721 float frac_y = fy - (float)iy;
722
723 /* Clamp neighbor indices for edge tiles */
724 int ix1 = (ix + 1 < map->width) ? ix + 1 : ix;
725 int iy1 = (iy + 1 < map->height) ? iy + 1 : iy;
726
727 float h00 = (float)iso_map_get_height(map, ix, iy);
728 float h10 = (float)iso_map_get_height(map, ix1, iy);
729 float h01 = (float)iso_map_get_height(map, ix, iy1);
730 float h11 = (float)iso_map_get_height(map, ix1, iy1);
731
732 float top = h00 + (h10 - h00) * frac_x;
733 float bot = h01 + (h11 - h01) * frac_x;
734 return top + (bot - top) * frac_y;
735} /* iso_map_interpolate_height() */
736
749void iso_map_calc_transitions(const ISO_MAP* map, int mx, int my, int* edge_bits, int* corner_bits) {
750 __n_assert(map, return);
751 __n_assert(edge_bits, return);
752 __n_assert(corner_bits, return);
753
754 int center = iso_map_get_terrain(map, mx, my);
755 *edge_bits = 0;
756 *corner_bits = 0;
757
758 /* Edge neighbors: W(-1,0), N(0,-1), E(+1,0), S(0,+1) */
759 int w = iso_map_get_terrain(map, mx - 1, my);
760 int n = iso_map_get_terrain(map, mx, my - 1);
761 int e = iso_map_get_terrain(map, mx + 1, my);
762 int s = iso_map_get_terrain(map, mx, my + 1);
763
764 if (w != center) *edge_bits |= ISO_EDGE_W;
765 if (n != center) *edge_bits |= ISO_EDGE_N;
766 if (e != center) *edge_bits |= ISO_EDGE_E;
767 if (s != center) *edge_bits |= ISO_EDGE_S;
768
769 /* Corner neighbors: only set if adjacent edges don't already have that terrain */
770 int nw = iso_map_get_terrain(map, mx - 1, my - 1);
771 int ne = iso_map_get_terrain(map, mx + 1, my - 1);
772 int se = iso_map_get_terrain(map, mx + 1, my + 1);
773 int sw = iso_map_get_terrain(map, mx - 1, my + 1);
774
775 if (nw != center && nw != w && nw != n) *corner_bits |= ISO_CORNER_NW;
776 if (ne != center && ne != n && ne != e) *corner_bits |= ISO_CORNER_NE;
777 if (se != center && se != e && se != s) *corner_bits |= ISO_CORNER_SE;
778 if (sw != center && sw != s && sw != w) *corner_bits |= ISO_CORNER_SW;
779} /* iso_map_calc_transitions() */
780
790int iso_map_should_transition(const ISO_MAP* map, int mx1, int my1, int mx2, int my2) {
791 __n_assert(map, return 0);
792 return iso_map_get_terrain(map, mx1, my1) != iso_map_get_terrain(map, mx2, my2);
793} /* iso_map_should_transition() */
794
808void iso_map_corner_heights(const ISO_MAP* map, int mx, int my, float* h_n, float* h_e, float* h_s, float* h_w) {
809 __n_assert(map, return);
810 __n_assert(h_n, return);
811 __n_assert(h_e, return);
812 __n_assert(h_s, return);
813 __n_assert(h_w, return);
814
815 float c = (float)iso_map_get_height(map, mx, my);
816 float nw = (float)iso_map_get_height(map, mx - 1, my - 1);
817 float nn = (float)iso_map_get_height(map, mx, my - 1);
818 float ne = (float)iso_map_get_height(map, mx + 1, my - 1);
819 float ee = (float)iso_map_get_height(map, mx + 1, my);
820 float se = (float)iso_map_get_height(map, mx + 1, my + 1);
821 float ss = (float)iso_map_get_height(map, mx, my + 1);
822 float sw = (float)iso_map_get_height(map, mx - 1, my + 1);
823 float ww = (float)iso_map_get_height(map, mx - 1, my);
824
825 *h_n = (c + nw + nn + ww) / 4.0f;
826 *h_e = (c + nn + ne + ee) / 4.0f;
827 *h_s = (c + ee + se + ss) / 4.0f;
828 *h_w = (c + ss + sw + ww) / 4.0f;
829} /* iso_map_corner_heights() */
830
839int iso_map_save(const ISO_MAP* map, const char* filename) {
840 __n_assert(map, return FALSE);
841 __n_assert(filename, return FALSE);
842
843 FILE* f = _iso_fopen_write(filename, "wb");
844 if (!f) {
845 n_log(LOG_ERR, "iso_map_save: could not open %s for writing", filename);
846 return FALSE;
847 }
848
849 /* magic */
850 fwrite("ISOM", 1, 4, f);
851
852 /* header */
853 const int32_t hdr[4] = {map->width, map->height, map->num_terrains, map->max_height};
854 fwrite(hdr, sizeof(int32_t), 4, f);
855
856 size_t total = (size_t)map->width * (size_t)map->height;
857
858 /* terrain layer */
859 fwrite(map->terrain, sizeof(int), total, f);
860 /* heightmap layer */
861 fwrite(map->heightmap, sizeof(int), total, f);
862 /* ability layer */
863 fwrite(map->ability, sizeof(int), total, f);
864
865 fclose(f);
866 return TRUE;
867} /* iso_map_save() */
868
874ISO_MAP* iso_map_load(const char* filename) {
875 __n_assert(filename, return NULL);
876
877 FILE* f = fopen(filename, "rb");
878 if (!f) {
879 n_log(LOG_ERR, "iso_map_load: could not open %s for reading", filename);
880 return NULL;
881 }
882
883 /* check magic */
884 char magic[4];
885 if (fread(magic, 1, 4, f) != 4 ||
886 magic[0] != 'I' || magic[1] != 'S' || magic[2] != 'O' || magic[3] != 'M') {
887 n_log(LOG_ERR, "iso_map_load: invalid file format");
888 fclose(f);
889 return NULL;
890 }
891
892 int32_t hdr[4];
893 if (fread(hdr, sizeof(int32_t), 4, f) != 4) {
894 n_log(LOG_ERR, "iso_map_load: truncated header");
895 fclose(f);
896 return NULL;
897 }
898
899 ISO_MAP* map = iso_map_new(hdr[0], hdr[1], hdr[2], hdr[3]);
900 if (!map) {
901 fclose(f);
902 return NULL;
903 }
904
905 size_t total = (size_t)map->width * (size_t)map->height;
906 size_t r = 0;
907 r = fread(map->terrain, sizeof(int), total, f);
908 if (r == total && !feof(f))
909 r += fread(map->heightmap, sizeof(int), total, f);
910 if (r == total * 2 && !feof(f))
911 r += fread(map->ability, sizeof(int), total, f);
912
913 if (r != total * 3) {
914 n_log(LOG_ERR, "iso_map_load: truncated data");
915 iso_map_free(&map);
916 fclose(f);
917 return NULL;
918 }
919
920 fclose(f);
921 return map;
922} /* iso_map_load() */
923
929 __n_assert(map, return);
930 for (int y = 0; y < map->height; y++) {
931 for (int x = 0; x < map->width; x++) {
932 int idx = y * map->width + x;
933 map->terrain[idx] = rand() % map->num_terrains;
934 map->heightmap[idx] = rand() % (map->max_height + 1);
935 map->ability[idx] = WALK;
936 }
937 }
938} /* iso_map_randomize() */
939
945static inline float _smooth_neighbor_h(const ISO_MAP* map, float h_self, int nx, int ny) {
946 float nh = (float)iso_map_get_height(map, nx, ny);
947 if (fabsf(h_self - nh) > (float)map->smooth_slope_max) return h_self;
948 return nh;
949}
950
963void 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) {
964 __n_assert(map, return);
965 __n_assert(h_n, return);
966 __n_assert(h_e, return);
967 __n_assert(h_s, return);
968 __n_assert(h_w, return);
969
970 float h = (float)iso_map_get_height(map, mx, my);
971
972 *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;
973
974 *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;
975
976 *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;
977
978 *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;
979} /* iso_map_smooth_corner_heights() */
980
981/* Per-tile segment API */
982
983int iso_map_set_segments(ISO_MAP* map, int mx, int my, const ISO_TILE_SEGMENT* segs, int count) {
984 __n_assert(map, return 0);
985 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
986 if (count < 0 || count > ISO_MAX_SEGMENTS_PER_TILE) return 0;
987
988 /* Validate invariants */
989 for (int i = 0; i < count; i++) {
990 if (segs[i].bottom >= segs[i].top) return 0;
991 if (i > 0 && segs[i - 1].top > segs[i].bottom) return 0;
992 }
993
994 /* Allocate segment array on first use */
995 if (!map->segments) {
996 size_t total = (size_t)map->width * (size_t)map->height;
997 Malloc(map->segments, ISO_TILE_SEGMENTS, total);
998 if (!map->segments) return 0;
999 memset(map->segments, 0, total * sizeof(ISO_TILE_SEGMENTS));
1000 }
1001
1002 int idx = my * map->width + mx;
1003 map->segments[idx].count = count;
1004 for (int i = 0; i < count; i++) map->segments[idx].segs[i] = segs[i];
1005 for (int i = count; i < ISO_MAX_SEGMENTS_PER_TILE; i++) {
1006 map->segments[idx].segs[i].bottom = 0;
1007 map->segments[idx].segs[i].top = 0;
1008 map->segments[idx].segs[i].upper_tile = -1;
1009 map->segments[idx].segs[i].lower_tile = -1;
1010 }
1011
1012 /* Update derived heightmap */
1013 map->heightmap[idx] = (count > 0) ? segs[count - 1].top : 0;
1014 /* Draw_order depends on segments + heights; invalidate. */
1015 map->draw_order_dirty = 1;
1016 return 1;
1017}
1018
1019const ISO_TILE_SEGMENTS* iso_map_get_segments(const ISO_MAP* map, int mx, int my) {
1020 if (!map || !map->segments) return NULL;
1021 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return NULL;
1022 return &map->segments[my * map->width + mx];
1023}
1024
1025static int _cmp_draw_entry(const void* a, const void* b) {
1026 const ISO_DRAW_ENTRY* ea = (const ISO_DRAW_ENTRY*)a;
1027 const ISO_DRAW_ENTRY* eb = (const ISO_DRAW_ENTRY*)b;
1028 /* Primary: isometric depth (back-to-front) */
1029 int depth_a = ea->my + ea->mx;
1030 int depth_b = eb->my + eb->mx;
1031 if (depth_a != depth_b) return depth_a - depth_b;
1032 /* Secondary: segment base height (lower drawn first) */
1033 if (ea->bottom != eb->bottom) return ea->bottom - eb->bottom;
1034 /* Tertiary: row then column */
1035 if (ea->my != eb->my) return ea->my - eb->my;
1036 return ea->mx - eb->mx;
1037}
1038
1039int iso_map_build_draw_order(const ISO_MAP* map, ISO_DRAW_ENTRY* out, int max_entries) {
1040 __n_assert(map, return 0);
1041 __n_assert(out, return 0);
1042
1043 int n = 0;
1044 for (int my = 0; my < map->height; my++) {
1045 for (int mx = 0; mx < map->width; mx++) {
1046 int idx = my * map->width + mx;
1047 if (map->segments && map->segments[idx].count > 0) {
1048 const ISO_TILE_SEGMENTS* ts = &map->segments[idx];
1049 for (int si = 0; si < ts->count && n < max_entries; si++) {
1050 out[n].mx = mx;
1051 out[n].my = my;
1052 out[n].seg_idx = si;
1053 out[n].bottom = ts->segs[si].bottom;
1054 out[n].top = ts->segs[si].top;
1055 /* underside: bottom > 0 and no segment directly below */
1056 out[n].underside = (ts->segs[si].bottom > 0 &&
1057 (si == 0 || ts->segs[si - 1].top < ts->segs[si].bottom));
1058 n++;
1059 }
1060 } else {
1061 /* No segments or count==0: single entry from heightmap */
1062 if (n >= max_entries) continue;
1063 int h = map->heightmap[idx];
1064 out[n].mx = mx;
1065 out[n].my = my;
1066 out[n].seg_idx = 0;
1067 out[n].bottom = 0;
1068 out[n].top = h;
1069 out[n].underside = 0;
1070 n++;
1071 }
1072 }
1073 }
1074
1075 qsort(out, (size_t)n, sizeof(ISO_DRAW_ENTRY), _cmp_draw_entry);
1076 return n;
1077}
1078
1085int iso_map_should_transition_smooth(const ISO_MAP* map, int mx1, int my1, int mx2, int my2) {
1086 __n_assert(map, return 0);
1087 int h1 = iso_map_get_height(map, mx1, my1);
1088 int h2 = iso_map_get_height(map, mx2, my2);
1089 if (map->smooth_height)
1090 return abs(h1 - h2) <= map->smooth_slope_max;
1091 return h1 == h2;
1092} /* iso_map_should_transition_smooth() */
1093
1109static int _neighbor_terrain_at(const ISO_MAP* map, int mx, int my) {
1110 if (my >= 0 && my < map->height) {
1111 if (mx == -1 && map->neighbor_terrains_west)
1112 return map->neighbor_terrains_west[my];
1113 if (mx == map->width && map->neighbor_terrains_east)
1114 return map->neighbor_terrains_east[my];
1115 }
1116 if (mx >= 0 && mx < map->width) {
1117 if (my == -1 && map->neighbor_terrains_north)
1118 return map->neighbor_terrains_north[mx];
1119 if (my == map->height && map->neighbor_terrains_south)
1120 return map->neighbor_terrains_south[mx];
1121 }
1122 return iso_map_get_terrain(map, mx, my);
1123}
1124
1125static int _neighbor_height_at(const ISO_MAP* map, int mx, int my) {
1126 if (my >= 0 && my < map->height) {
1127 if (mx == -1 && map->neighbor_heights_west)
1128 return map->neighbor_heights_west[my];
1129 if (mx == map->width && map->neighbor_heights_east)
1130 return map->neighbor_heights_east[my];
1131 }
1132 if (mx >= 0 && mx < map->width) {
1133 if (my == -1 && map->neighbor_heights_north)
1134 return map->neighbor_heights_north[mx];
1135 if (my == map->height && map->neighbor_heights_south)
1136 return map->neighbor_heights_south[mx];
1137 }
1138 return iso_map_get_height(map, mx, my);
1139}
1140
1141static int _neighbor_should_transition(const ISO_MAP* map, int mx1, int my1, int mx2, int my2) {
1142 int h1 = _neighbor_height_at(map, mx1, my1);
1143 int h2 = _neighbor_height_at(map, mx2, my2);
1144 if (map->smooth_height)
1145 return abs(h1 - h2) <= map->smooth_slope_max;
1146 return h1 == h2;
1147}
1148
1160void iso_map_calc_transitions_full(const ISO_MAP* map, int mx, int my, int* edge_bits, int* corner_bits) {
1161 __n_assert(map, return);
1162 __n_assert(edge_bits, return);
1163 __n_assert(corner_bits, return);
1164
1165 int base = iso_map_get_terrain(map, mx, my);
1166 memset(edge_bits, 0, sizeof(int) * (size_t)map->num_terrains);
1167 memset(corner_bits, 0, sizeof(int) * (size_t)map->num_terrains);
1168
1169 /* Cardinal neighbors: only if height-connected. Uses the
1170 * _neighbor_* helpers so chunk-edge lookups consult the
1171 * neighbor_terrains_* / neighbor_heights_* arrays instead of
1172 * falling to the default 0-out-of-bounds and dropping the
1173 * cross-chunk transition. */
1174 int t_w = _neighbor_should_transition(map, mx, my, mx - 1, my)
1175 ? _neighbor_terrain_at(map, mx - 1, my)
1176 : base;
1177 int t_n = _neighbor_should_transition(map, mx, my, mx, my - 1)
1178 ? _neighbor_terrain_at(map, mx, my - 1)
1179 : base;
1180 int t_e = _neighbor_should_transition(map, mx, my, mx + 1, my)
1181 ? _neighbor_terrain_at(map, mx + 1, my)
1182 : base;
1183 int t_s = _neighbor_should_transition(map, mx, my, mx, my + 1)
1184 ? _neighbor_terrain_at(map, mx, my + 1)
1185 : base;
1186
1187 /* Diagonal neighbors, see _neighbor_terrain_at comment. Diagonals
1188 * that fall off the chunk still return 0 (water); the visible
1189 * fallout is single-tile corner artifacts at chunk boundaries
1190 * rather than whole-edge seams. */
1191 int t_nw = _neighbor_should_transition(map, mx, my, mx - 1, my - 1)
1192 ? _neighbor_terrain_at(map, mx - 1, my - 1)
1193 : base;
1194 int t_ne = _neighbor_should_transition(map, mx, my, mx + 1, my - 1)
1195 ? _neighbor_terrain_at(map, mx + 1, my - 1)
1196 : base;
1197 int t_se = _neighbor_should_transition(map, mx, my, mx + 1, my + 1)
1198 ? _neighbor_terrain_at(map, mx + 1, my + 1)
1199 : base;
1200 int t_sw = _neighbor_should_transition(map, mx, my, mx - 1, my + 1)
1201 ? _neighbor_terrain_at(map, mx - 1, my + 1)
1202 : base;
1203
1204 for (int t = base + 1; t < map->num_terrains; t++) {
1205 if (t_w == t) edge_bits[t] |= ISO_EDGE_W;
1206 if (t_n == t) edge_bits[t] |= ISO_EDGE_N;
1207 if (t_e == t) edge_bits[t] |= ISO_EDGE_E;
1208 if (t_s == t) edge_bits[t] |= ISO_EDGE_S;
1209
1210 if (t_nw == t && !(edge_bits[t] & ISO_EDGE_W) && !(edge_bits[t] & ISO_EDGE_N))
1211 corner_bits[t] |= ISO_CORNER_NW;
1212 if (t_ne == t && !(edge_bits[t] & ISO_EDGE_N) && !(edge_bits[t] & ISO_EDGE_E))
1213 corner_bits[t] |= ISO_CORNER_NE;
1214 if (t_se == t && !(edge_bits[t] & ISO_EDGE_E) && !(edge_bits[t] & ISO_EDGE_S))
1215 corner_bits[t] |= ISO_CORNER_SE;
1216 if (t_sw == t && !(edge_bits[t] & ISO_EDGE_S) && !(edge_bits[t] & ISO_EDGE_W))
1217 corner_bits[t] |= ISO_CORNER_SW;
1218 }
1219} /* iso_map_calc_transitions_full() */
1220
1221/* N_ISO_CAMERA */
1222
1226N_ISO_CAMERA* n_iso_camera_new(float zoom_min, float zoom_max) {
1227 N_ISO_CAMERA* cam = NULL;
1228 Malloc(cam, N_ISO_CAMERA, 1);
1229 __n_assert(cam, return NULL);
1230 cam->x = 0.0f;
1231 cam->y = 0.0f;
1232 cam->zoom = 1.0f;
1233 cam->zoom_min = zoom_min;
1234 cam->zoom_max = zoom_max;
1235 return cam;
1236} /* n_iso_camera_new() */
1237
1242 __n_assert(cam && *cam, return);
1243 Free(*cam);
1244} /* n_iso_camera_free() */
1245
1249void n_iso_camera_scroll(N_ISO_CAMERA* cam, float dx, float dy) {
1250 __n_assert(cam, return);
1251 cam->x += dx;
1252 cam->y += dy;
1253} /* n_iso_camera_scroll() */
1254
1260void n_iso_camera_zoom(N_ISO_CAMERA* cam, float dz, float mouse_x, float mouse_y) {
1261 __n_assert(cam, return);
1262 float old_zoom = cam->zoom;
1263 cam->zoom += dz;
1264 if (cam->zoom < cam->zoom_min) cam->zoom = cam->zoom_min;
1265 if (cam->zoom > cam->zoom_max) cam->zoom = cam->zoom_max;
1266 /* Adjust position so the world point under the mouse stays fixed */
1267 cam->x += mouse_x / cam->zoom - mouse_x / old_zoom;
1268 cam->y += mouse_y / cam->zoom - mouse_y / old_zoom;
1269} /* n_iso_camera_zoom() */
1270
1274void n_iso_camera_center_on(N_ISO_CAMERA* cam, float world_x, float world_y, int screen_w, int screen_h) {
1275 __n_assert(cam, return);
1276 cam->x = -world_x + (float)screen_w / (2.0f * cam->zoom);
1277 cam->y = -world_y + (float)screen_h / (2.0f * cam->zoom);
1278} /* n_iso_camera_center_on() */
1279
1283void n_iso_camera_follow(N_ISO_CAMERA* cam, float target_x, float target_y, int screen_w, int screen_h, float smoothing, float dt) {
1284 __n_assert(cam, return);
1285 float target_cx = -target_x + (float)screen_w / (2.0f * cam->zoom);
1286 float target_cy = -target_y + (float)screen_h / (2.0f * cam->zoom);
1287 float t = smoothing * dt;
1288 if (t > 1.0f) t = 1.0f;
1289 cam->x += (target_cx - cam->x) * t;
1290 cam->y += (target_cy - cam->y) * t;
1291} /* n_iso_camera_follow() */
1292
1296void n_iso_camera_screen_to_world(const N_ISO_CAMERA* cam, float sx, float sy, float* wx, float* wy) {
1297 __n_assert(cam, return);
1298 __n_assert(wx, return);
1299 __n_assert(wy, return);
1300 *wx = sx / cam->zoom - cam->x;
1301 *wy = sy / cam->zoom - cam->y;
1302} /* n_iso_camera_screen_to_world() */
1303
1307void n_iso_camera_world_to_screen(const N_ISO_CAMERA* cam, float wx, float wy, float* sx, float* sy) {
1308 __n_assert(cam, return);
1309 __n_assert(sx, return);
1310 __n_assert(sy, return);
1311 *sx = (wx + cam->x) * cam->zoom;
1312 *sy = (wy + cam->y) * cam->zoom;
1313} /* n_iso_camera_world_to_screen() */
1314
1315/* Height border edge visibility (public API, no Allegro dependency) */
1316
1330 ISO_VISIBLE_EDGES edges = {0};
1331 if (!map) return edges;
1332
1333 int h = iso_map_get_height(map, mx, my);
1334 if (h == 0) return edges;
1335
1336 /* NW edge: neighbor (mx-1, my) */
1337 if (mx <= 0) {
1338 /* At west boundary: use neighbor chunk data if available */
1339 if (map->neighbor_heights_west) {
1340 if (abs(h - map->neighbor_heights_west[my]) >= 1)
1341 edges.draw_nw = 1;
1342 } else {
1343 edges.draw_nw = 1; /* world boundary */
1344 }
1345 } else if (abs(h - iso_map_get_height(map, mx - 1, my)) >= 1) {
1346 edges.draw_nw = 1;
1347 }
1348
1349 /* NE edge: neighbor (mx, my-1) */
1350 if (my <= 0) {
1351 if (map->neighbor_heights_north) {
1352 if (abs(h - map->neighbor_heights_north[mx]) >= 1)
1353 edges.draw_ne = 1;
1354 } else {
1355 edges.draw_ne = 1;
1356 }
1357 } else if (abs(h - iso_map_get_height(map, mx, my - 1)) >= 1) {
1358 edges.draw_ne = 1;
1359 }
1360
1361 /* SE edge: neighbor (mx+1, my) */
1362 if (mx >= map->width - 1) {
1363 if (map->neighbor_heights_east) {
1364 if (abs(h - map->neighbor_heights_east[my]) >= 1)
1365 edges.draw_se = 1;
1366 } else {
1367 edges.draw_se = 1;
1368 }
1369 } else if (abs(h - iso_map_get_height(map, mx + 1, my)) >= 1) {
1370 edges.draw_se = 1;
1371 }
1372
1373 /* SW edge: neighbor (mx, my+1) */
1374 if (my >= map->height - 1) {
1375 if (map->neighbor_heights_south) {
1376 if (abs(h - map->neighbor_heights_south[mx]) >= 1)
1377 edges.draw_sw = 1;
1378 } else {
1379 edges.draw_sw = 1;
1380 }
1381 } else if (abs(h - iso_map_get_height(map, mx, my + 1)) >= 1) {
1382 edges.draw_sw = 1;
1383 }
1384
1385 return edges;
1386}
1387
1401static int _tile_covers_height(const ISO_MAP* map, int mx, int my, int z) {
1402 if (!map || mx < 0 || mx >= map->width || my < 0 || my >= map->height)
1403 return 0;
1404 const ISO_TILE_SEGMENTS* ts = iso_map_get_segments(map, mx, my);
1405 if (ts && ts->count > 0) {
1406 for (int i = 0; i < ts->count; i++) {
1407 if (ts->segs[i].bottom <= z && ts->segs[i].top > z + 1)
1408 return 1;
1409 }
1410 return 0;
1411 }
1412 int h = iso_map_get_height(map, mx, my);
1413 return (h > z + 1) ? 1 : 0;
1414}
1415
1440ISO_VISIBLE_EDGES iso_map_get_visible_edges_segment(const ISO_MAP* map, int mx, int my, int seg_idx) {
1441 ISO_VISIBLE_EDGES edges = {0};
1442 if (!map) return edges;
1443
1444 /* Get this tile's segment */
1445 ISO_TILE_SEGMENT seg;
1446 const ISO_TILE_SEGMENTS* ts = iso_map_get_segments(map, mx, my);
1447 if (ts && seg_idx >= 0 && seg_idx < ts->count) {
1448 seg = ts->segs[seg_idx];
1449 } else {
1450 /* Fallback: heightmap as single segment {0, h} */
1451 int h = iso_map_get_height(map, mx, my);
1452 if (h == 0) return edges;
1453 seg.bottom = 0;
1454 seg.top = h;
1455 }
1456
1457 if (seg.top <= seg.bottom) return edges;
1458
1459 /* Direction offsets: NW->(mx-1,my), NE->(mx,my-1), SE->(mx+1,my), SW->(mx,my+1) */
1460 static const int ddx[4] = {-1, 0, 1, 0};
1461 static const int ddy[4] = {0, -1, 0, 1};
1462 int* flags[4];
1463 flags[0] = &edges.draw_nw;
1464 flags[1] = &edges.draw_ne;
1465 flags[2] = &edges.draw_se;
1466 flags[3] = &edges.draw_sw;
1467
1468 int any_side_exposed = 0; /* for underside check */
1469 int ntop[4] = {0, 0, 0, 0}; /* neighbor top height per direction */
1470
1471 for (int d = 0; d < 4; d++) {
1472 int nx = mx + ddx[d];
1473 int ny = my + ddy[d];
1474 int neighbor_matches = 0;
1475 int neighbor_covers_bottom = 0;
1476
1477 if (nx < 0 || nx >= map->width || ny < 0 || ny >= map->height) {
1478 /* Boundary: check neighbor chunk height arrays */
1479 int nh = -1;
1480 if (d == 0 && map->neighbor_heights_west) nh = map->neighbor_heights_west[my];
1481 if (d == 1 && map->neighbor_heights_north) nh = map->neighbor_heights_north[mx];
1482 if (d == 2 && map->neighbor_heights_east) nh = map->neighbor_heights_east[my];
1483 if (d == 3 && map->neighbor_heights_south) nh = map->neighbor_heights_south[mx];
1484
1485 if (nh > 0) {
1486 /* Boundary treated as single segment {0,nh}: match if same top */
1487 neighbor_matches = (nh == seg.top);
1488 neighbor_covers_bottom = (nh >= seg.bottom);
1489 ntop[d] = nh;
1490 }
1491 /* else: world boundary -> no match, not covered, ntop stays 0 */
1492 } else {
1493 const ISO_TILE_SEGMENTS* nts = iso_map_get_segments(map, nx, ny);
1494 if (nts && nts->count > 0) {
1495 /* Match if any neighbour segment has the same top height.
1496 * This applies uniformly to ground and floating segments:
1497 * same top = connected surface = no border.
1498 * Different top = visible height step = border drawn. */
1499 for (int si = 0; si < nts->count; si++) {
1500 if (nts->segs[si].top == seg.top) {
1501 neighbor_matches = 1;
1502 }
1503 if (nts->segs[si].top >= seg.bottom) {
1504 neighbor_covers_bottom = 1;
1505 }
1506 }
1507 /* Use heightmap value as neighbor top for vertical lines */
1508 ntop[d] = iso_map_get_height(map, nx, ny);
1509 } else {
1510 /* No segment data: heightmap as single segment {0, h} */
1511 int nh = iso_map_get_height(map, nx, ny);
1512 if (nh > 0) {
1513 neighbor_matches = (nh == seg.top);
1514 neighbor_covers_bottom = (nh >= seg.bottom);
1515 }
1516 ntop[d] = nh;
1517 }
1518 }
1519
1520 if (!neighbor_matches) *flags[d] = 1;
1521 if (!neighbor_covers_bottom) any_side_exposed = 1;
1522 }
1523
1524 /* OCCLUSION: NW/NE edges face away from camera.
1525 * Suppress if the tile drawn later (south in painter order) has
1526 * geometry covering this segment's top face height.
1527 * NW edge: covered by SW neighbour (mx, my+1)
1528 * NE edge: covered by SE neighbour (mx+1, my) */
1529 if (edges.draw_nw && _tile_covers_height(map, mx, my + 1, seg.top))
1530 edges.draw_nw = 0;
1531 if (edges.draw_ne && _tile_covers_height(map, mx + 1, my, seg.top))
1532 edges.draw_ne = 0;
1533
1534 /* Underside border for floating segments */
1535 if (seg.bottom > 0 && any_side_exposed) {
1536 edges.draw_underside = 1;
1537 }
1538
1539 /* Store neighbor top heights for vertical corner line computation */
1540 edges.neighbor_top_nw = ntop[0];
1541 edges.neighbor_top_ne = ntop[1];
1542 edges.neighbor_top_se = ntop[2];
1543 edges.neighbor_top_sw = ntop[3];
1544
1545 return edges;
1546}
1547
1548/* ISO_MAP rendering (requires Allegro 5) */
1549#ifdef HAVE_ALLEGRO
1550
1551#include <allegro5/allegro.h>
1552#include <allegro5/allegro_primitives.h>
1553
1579static 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) {
1580 float wx = (float)(cx - cy) * hw + 2.0f * hw;
1581 float wy = (float)(cx + cy) * hh - fh * tl;
1582 *sx = wx * zoom + cam_px;
1583 *sy = wy * zoom + cam_py;
1584}
1585
1586/* Extended tint: combines height tint with ambient color and per-tile dynamic light.
1587 * ambient_r/g/b: global day/night ambient (1.0 = full daylight).
1588 * dyn_r/g/b: additive dynamic light contribution at this tile (0.0 = no light). */
1589static 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) {
1590 /* Start with height-based brightness */
1591 float hb = 1.0f;
1592 if (intensity > 0.0f && max_height > 0) {
1593 float ratio = (float)seg_top / (float)max_height;
1594 hb = 1.0f - intensity * (1.0f - ratio);
1595 if (hb < 0.0f) hb = 0.0f;
1596 if (hb > 1.0f) hb = 1.0f;
1597 }
1598
1599 /* Multiply by ambient color */
1600 float r = hb * ambient_r;
1601 float g = hb * ambient_g;
1602 float b = hb * ambient_b;
1603
1604 /* Add dynamic light (additive boost) */
1605 r += dyn_r;
1606 g += dyn_g;
1607 b += dyn_b;
1608
1609 /* Clamp to avoid overexposure */
1610 if (r > 1.0f) r = 1.0f;
1611 if (g > 1.0f) g = 1.0f;
1612 if (b > 1.0f) b = 1.0f;
1613
1614 /* The tint multiplier is sRGB-designed (ambient curve + dyn-light
1615 * literals tuned on a non-sRGB display). Route through the primitive
1616 * color filter so an sRGB-aware consumer can linearise it before it
1617 * multiplies the linear-decoded texel inside an sRGB FBO. Default
1618 * filter = identity, so the non-sRGB path is byte-identical. */
1619 return iso_apply_primitive_color_filter(al_map_rgba_f(r, g, b, 1.0f));
1620}
1621
1628static void _draw_tile_warped(ALLEGRO_BITMAP* bmp,
1629 const float vx[4],
1630 const float vy[4],
1631 int tile_w,
1632 int tile_h,
1633 ALLEGRO_COLOR tint) {
1634 /* al_draw_prim samples u/v against the PARENT texture and does NOT
1635 * auto-translate for a sub-bitmap (unlike al_draw_*_bitmap). When tiles
1636 * are packed into a terrain atlas (each tile an al_create_sub_bitmap),
1637 * passing sub-local UVs [0..tile_w] samples the atlas ORIGIN, which is
1638 * transparent padding, so warped (sloped) tiles render BLACK. Resolve
1639 * the parent + add the sub-bitmap's atlas offset to every UV and draw
1640 * against the parent so the warp samples the tile's real atlas region.
1641 * For a standalone (non-sub) bitmap the offsets are 0 and parent == bmp,
1642 * so this is byte-identical to the pre-atlas behaviour. */
1643 ALLEGRO_BITMAP* tex = al_get_parent_bitmap(bmp);
1644 float ox, oy;
1645 if (tex) {
1646 ox = (float)al_get_bitmap_x(bmp);
1647 oy = (float)al_get_bitmap_y(bmp);
1648 } else {
1649 tex = bmp;
1650 ox = 0.0f;
1651 oy = 0.0f;
1652 }
1653 float un = ox + (float)tile_w / 2.0f, vn = oy + 0.0f;
1654 float ue = ox + (float)tile_w, ve = oy + (float)tile_h / 2.0f;
1655 float us = ox + (float)tile_w / 2.0f, vs = oy + (float)tile_h;
1656 float uw = ox + 0.0f, vw = oy + (float)tile_h / 2.0f;
1657 ALLEGRO_VERTEX vtx[6];
1658 vtx[0].x = vx[0];
1659 vtx[0].y = vy[0];
1660 vtx[0].z = 0;
1661 vtx[0].u = un;
1662 vtx[0].v = vn;
1663 vtx[0].color = tint;
1664 vtx[1].x = vx[1];
1665 vtx[1].y = vy[1];
1666 vtx[1].z = 0;
1667 vtx[1].u = ue;
1668 vtx[1].v = ve;
1669 vtx[1].color = tint;
1670 vtx[2].x = vx[2];
1671 vtx[2].y = vy[2];
1672 vtx[2].z = 0;
1673 vtx[2].u = us;
1674 vtx[2].v = vs;
1675 vtx[2].color = tint;
1676 vtx[3].x = vx[0];
1677 vtx[3].y = vy[0];
1678 vtx[3].z = 0;
1679 vtx[3].u = un;
1680 vtx[3].v = vn;
1681 vtx[3].color = tint;
1682 vtx[4].x = vx[2];
1683 vtx[4].y = vy[2];
1684 vtx[4].z = 0;
1685 vtx[4].u = us;
1686 vtx[4].v = vs;
1687 vtx[4].color = tint;
1688 vtx[5].x = vx[3];
1689 vtx[5].y = vy[3];
1690 vtx[5].z = 0;
1691 vtx[5].u = uw;
1692 vtx[5].v = vw;
1693 vtx[5].color = tint;
1694
1695 al_draw_prim(vtx, NULL, tex, 0, 6, ALLEGRO_PRIM_TRIANGLE_LIST);
1696}
1697
1703static void _draw_segment_sides(const ISO_MAP* map,
1704 float east_x,
1705 float east_y,
1706 float south_x,
1707 float south_y,
1708 float west_x,
1709 float west_y,
1710 int mx,
1711 int my,
1712 int seg_bottom,
1713 int seg_top,
1714 float lift,
1715 float height_brightness,
1716 int wall_terrain_override) {
1717 int base = (wall_terrain_override >= 0) ? wall_terrain_override
1718 : iso_map_get_terrain(map, mx, my);
1719 float east_shade = 0.55f;
1720 float south_shade = 0.35f;
1721
1722 float cr, cg, cb;
1723 switch (base) {
1724 case 0:
1725 cr = 0.7f;
1726 cg = 0.25f;
1727 cb = 0.1f;
1728 break;
1729 case 1:
1730 cr = 0.5f;
1731 cg = 0.45f;
1732 cb = 0.35f;
1733 break;
1734 case 2:
1735 cr = 0.6f;
1736 cg = 0.55f;
1737 cb = 0.45f;
1738 break;
1739 default:
1740 cr = 0.4f;
1741 cg = 0.35f;
1742 cb = 0.3f;
1743 break;
1744 }
1745 ALLEGRO_COLOR east_col = iso_apply_primitive_color_filter(
1746 al_map_rgba_f(cr * east_shade * height_brightness,
1747 cg * east_shade * height_brightness,
1748 cb * east_shade * height_brightness, 1.0f));
1749 ALLEGRO_COLOR south_col = iso_apply_primitive_color_filter(
1750 al_map_rgba_f(cr * south_shade * height_brightness,
1751 cg * south_shade * height_brightness,
1752 cb * south_shade * height_brightness, 1.0f));
1753 /* Keep PASS 1 on ONE blender throughout to avoid held-batch
1754 * cross-tile contamination. Cliff wall fills use alpha=1.0 ->
1755 * premul == straight, and edge lines are RGB=0 -> premul ==
1756 * straight, so this switch is a visual no-op. */
1757 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
1758
1759 int east_face_h, south_face_h;
1760 if (seg_bottom == 0) {
1761 /* Ground segment: use neighbor-aware wall height (existing logic) */
1762 int h_e = iso_map_get_height(map, mx + 1, my);
1763 int h_s = iso_map_get_height(map, mx, my + 1);
1764 int h_se = iso_map_get_height(map, mx + 1, my + 1);
1765
1766 east_face_h = seg_top - (h_e < h_se ? h_e : h_se);
1767 // cppcheck-suppress redundantAssignment ; fallback chain: min(h_e,h_se) -> h_e -> h_se
1768 if (east_face_h < 1) east_face_h = seg_top - h_e;
1769 if (east_face_h < 1) east_face_h = seg_top - h_se;
1770
1771 south_face_h = seg_top - (h_s < h_se ? h_s : h_se);
1772 // cppcheck-suppress redundantAssignment ; fallback chain: min(h_s,h_se) -> h_s -> h_se
1773 if (south_face_h < 1) south_face_h = seg_top - h_s;
1774 if (south_face_h < 1) south_face_h = seg_top - h_se;
1775 } else {
1776 /* Floating segment: full wall from bottom to top */
1777 east_face_h = seg_top - seg_bottom;
1778 south_face_h = seg_top - seg_bottom;
1779 }
1780
1781 /* No sub-pixel expansion needed: all vertices come from the canonical
1782 * _iso_corner_to_screen formula, so adjacent tiles sharing an edge
1783 * produce bit-identical vertex positions. */
1784
1785 /* Query the client-registered jitter source. Returns (0, 0) when
1786 * no jitter source is active. When non-zero, we subtract it from
1787 * every line endpoint so the line lands at the same screen pixel
1788 * each frame regardless of where in the TAA jitter cycle we are.
1789 * Filled polygon faces deliberately KEEP the jitter: they are
1790 * the larger draws TAA's history blend smooths perceptually. */
1791 float jx = 0.0f, jy = 0.0f;
1793
1794 if (east_face_h > 0) {
1795 float drop = (float)east_face_h * lift;
1796 float v[8] = {east_x, east_y, south_x, south_y,
1797 south_x, south_y + drop,
1798 east_x, east_y + drop};
1799 al_draw_filled_polygon(v, 4, east_col);
1800 ALLEGRO_COLOR edge = al_map_rgba(0, 0, 0, 80);
1801 al_draw_line(east_x - jx, east_y + drop - jy,
1802 south_x - jx, south_y + drop - jy,
1803 edge, 1.0f);
1804 }
1805
1806 if (south_face_h > 0) {
1807 float drop = (float)south_face_h * lift;
1808 float v[8] = {south_x, south_y, west_x, west_y,
1809 west_x, west_y + drop,
1810 south_x, south_y + drop};
1811 al_draw_filled_polygon(v, 4, south_col);
1812 ALLEGRO_COLOR edge = al_map_rgba(0, 0, 0, 80);
1813 al_draw_line(west_x - jx, west_y + drop - jy,
1814 south_x - jx, south_y + drop - jy,
1815 edge, 1.0f);
1816 }
1817}
1818
1823static void _draw_segment_underside(ALLEGRO_BITMAP* bmp,
1824 float base_dx,
1825 float base_dy,
1826 float phw,
1827 float phh,
1828 float tile_draw_w,
1829 float tile_draw_h,
1830 int tile_w,
1831 int tile_h) {
1832 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
1833 /* Draw the tile bitmap with a darkened tint (65% brightness) */
1834 ALLEGRO_COLOR tint = al_map_rgba_f(0.65f, 0.65f, 0.65f, 1.0f);
1835 (void)phw;
1836 (void)phh;
1837 al_draw_tinted_scaled_bitmap(bmp, tint,
1838 0, 0, (float)tile_w, (float)tile_h,
1839 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
1840}
1841
1863 float obj_fx,
1864 float obj_fy,
1865 float obj_fz,
1866 float obj_sx,
1867 float obj_sy,
1868 float sprite_h,
1869 float sprite_half_w,
1870 float cam_px,
1871 float cam_py,
1872 float zoom,
1873 float* out_clip_y) {
1874 /* Hoist projection constants out of the inner loop. */
1875 const float proj_hw = map->proj.half_w;
1876 const float proj_hh = map->proj.half_h;
1877 const float proj_lift = map->proj.tile_lift;
1878
1879 float phw = proj_hw * zoom;
1880 float phh = proj_hh * zoom;
1881 float lift = proj_lift * zoom;
1882
1883 /* Entity head screen Y: sprite_h world units above feet */
1884 float head_sy = obj_sy - sprite_h * lift;
1885 /* Entity horizontal extent (screen pixels) */
1886 float ent_left = obj_sx - sprite_half_w * zoom;
1887 float ent_right = obj_sx + sprite_half_w * zoom;
1888
1889 int base_mx = (int)floorf(obj_fx);
1890 int base_my = (int)floorf(obj_fy);
1891 int check_range = map->max_height + 2;
1892 int found = 0;
1893 float best_clip_y = 1e9f;
1894
1895 for (int dy = -1; dy <= check_range; dy++) {
1896 for (int dx = -1; dx <= check_range; dx++) {
1897 /* Only check tiles drawn after the object in painter's order.
1898 * Filter by isometric depth (dx+dy) not just grid position. */
1899 int tdepth = dx + dy;
1900 if (tdepth < 0) continue;
1901 if (tdepth == 0 && dy < 0) continue;
1902 if (tdepth == 0 && dy == 0 && dx <= 0) continue;
1903
1904 int tmx = base_mx + dx;
1905 int tmy = base_my + dy;
1906 if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height) continue;
1907
1908 int th = iso_map_get_height(map, tmx, tmy);
1909 /* tile must reach above the entity's feet to potentially occlude */
1910 if ((float)th <= obj_fz) continue;
1911
1912 /* inlined iso_map_to_screen(map, tmx, tmy, th, &tsx, &tsy) */
1913 float tsx = ((float)tmx - (float)tmy) * proj_hw + proj_hw;
1914 float tsy = ((float)tmx + (float)tmy) * proj_hh - (float)th * proj_lift;
1915 float tile_dx = tsx * zoom + cam_px;
1916 float tile_dy = tsy * zoom + cam_py;
1917
1918 /* tile screen bounding box */
1919 float tile_left = tile_dx;
1920 float tile_right = tile_dx + 2.0f * phw;
1921 float tile_top = tile_dy;
1922 float tile_bot = tile_dy + 2.0f * phh + (float)th * lift;
1923
1924 /* Check horizontal overlap (entity range vs tile range) */
1925 if (ent_right < tile_left || ent_left > tile_right) continue;
1926
1927 /* Check vertical overlap (any part of entity behind tile) */
1928 if (tile_top <= obj_sy && tile_bot >= head_sy) {
1929 found = 1;
1930 /* Track north vertex of occluding tile for clip-Y */
1931 float nx, ny;
1932 _iso_corner_to_screen(proj_hw, proj_hh, proj_lift,
1933 tmx, tmy, (float)th,
1934 cam_px, cam_py, zoom, &nx, &ny);
1935 if (ny < best_clip_y) best_clip_y = ny;
1936 }
1937 }
1938 }
1939 if (out_clip_y) *out_clip_y = best_clip_y;
1940 return found;
1941}
1942
1956static void _draw_height_borders(const ISO_MAP* map,
1957 int mx,
1958 int my,
1959 int seg_idx,
1960 int seg_bottom,
1961 int seg_top,
1962 int has_underside_face,
1963 const float vx[4],
1964 const float vy[4],
1965 float lift) {
1966 ISO_VISIBLE_EDGES edges = iso_map_get_visible_edges_segment(map, mx, my, seg_idx);
1967 if (!edges.draw_nw && !edges.draw_ne && !edges.draw_se && !edges.draw_sw && !(has_underside_face && edges.draw_underside))
1968 return;
1969
1970 /* Subtract the client-registered jitter (e.g. the consumer's TAA
1971 * sub-pixel offset) from every line endpoint so these decorative
1972 * borders stay pixel-stable across frames while sprites and
1973 * filled faces continue to inherit the jitter. Returns (0, 0)
1974 * when no jitter source is active. */
1975 float jx = 0.0f, jy = 0.0f;
1977
1978 ALLEGRO_COLOR border_col = al_map_rgba(0, 0, 0, 200);
1979 float thickness = 1.5f;
1980
1981 /* Top face edges */
1982 if (edges.draw_se)
1983 al_draw_line(vx[1] - jx, vy[1] - jy,
1984 vx[2] - jx, vy[2] - jy, border_col, thickness);
1985 if (edges.draw_sw)
1986 al_draw_line(vx[2] - jx, vy[2] - jy,
1987 vx[3] - jx, vy[3] - jy, border_col, thickness);
1988 if (edges.draw_nw)
1989 al_draw_line(vx[0] - jx, vy[0] - jy,
1990 vx[3] - jx, vy[3] - jy, border_col, thickness);
1991 if (edges.draw_ne)
1992 al_draw_line(vx[0] - jx, vy[0] - jy,
1993 vx[1] - jx, vy[1] - jy, border_col, thickness);
1994
1995 /* Vertical corner lines.
1996 *
1997 * For each drawn edge where seg_top > neighbor_top (A is higher):
1998 * Draw a vertical line at BOTH endpoints of that edge,
1999 * from tileToScreen(vertex, seg_top) down to
2000 * tileToScreen(vertex, neighbor_top).
2001 * Exception: NEVER draw at the North vertex (vx[0]/vy[0]).
2002 *
2003 * If seg_top <= neighbor_top: no vertical lines (wall belongs to N).
2004 *
2005 * vx[]/vy[] are already at tileToScreen(vertex, seg_top).
2006 * The bottom endpoint is (seg_top - HN) * lift pixels below.
2007 *
2008 * Diamond vertices: N=0, E=1, S=2, W=3.
2009 * Edge endpoints: NE=(N,E) SE=(E,S) SW=(S,W) NW=(W,N)
2010 *
2011 * Multiple edges may want to draw at the same vertex with different
2012 * drops. We draw the longest line (smallest HN) at each vertex
2013 * to avoid duplicate shorter lines.
2014 */
2015 {
2016 /* Per-vertex: track the largest drop (longest line) requested.
2017 *
2018 * CONTINUITY CHECK: a vertical corner line at a vertex is only
2019 * drawn if the wall face ENDS at that vertex, i.e. the adjacent
2020 * tile in the wall's direction is NOT at the same height.
2021 * If the adjacent tile IS at the same height, the wall surface
2022 * continues past the vertex and the line is interior (hidden).
2023 *
2024 * For the east-facing wall (SE edge):
2025 * E vertex: wall continues northward if NE neighbor same height -> skip
2026 * S vertex: wall continues southward if SW neighbor same height -> skip
2027 * For the south-facing wall (SW edge):
2028 * S vertex: wall continues eastward if SE neighbor same height -> skip
2029 * W vertex: wall continues westward if NW neighbor same height -> skip
2030 * For NE edge -> E vertex: wall continues if SE neighbor same height
2031 * For NW edge -> W vertex: wall continues if SW neighbor same height
2032 */
2033 float drop_e = 0, drop_s = 0, drop_w = 0;
2034 int nt_nw = edges.neighbor_top_nw;
2035 int nt_ne = edges.neighbor_top_ne;
2036 int nt_se = edges.neighbor_top_se;
2037 int nt_sw = edges.neighbor_top_sw;
2038
2039 /* Clamp neighbor tops to seg_bottom: vertical lines must not
2040 * extend below the segment's own base (floating segments). */
2041 int bot = seg_bottom;
2042 int cl_nw = nt_nw > bot ? nt_nw : bot;
2043 int cl_ne = nt_ne > bot ? nt_ne : bot;
2044 int cl_se = nt_se > bot ? nt_se : bot;
2045 int cl_sw = nt_sw > bot ? nt_sw : bot;
2046
2047 /* NE edge (N,E): only E vertex (N is never drawn) */
2048 if (edges.draw_ne && seg_top > cl_ne) {
2049 /* E vertex: east wall continues if SE neighbor same height */
2050 if (nt_se != seg_top) {
2051 float d = (float)(seg_top - cl_ne) * lift;
2052 if (d > drop_e) drop_e = d;
2053 }
2054 }
2055 /* SE edge (E,S): E and S vertices */
2056 if (edges.draw_se && seg_top > cl_se) {
2057 float d = (float)(seg_top - cl_se) * lift;
2058 /* E vertex: east wall continues northward if NE neighbor same height */
2059 if (nt_ne != seg_top) {
2060 if (d > drop_e) drop_e = d;
2061 }
2062 /* S vertex: east wall continues southward if SW neighbor same height */
2063 if (nt_sw != seg_top) {
2064 if (d > drop_s) drop_s = d;
2065 }
2066 }
2067 /* SW edge (S,W): S and W vertices */
2068 if (edges.draw_sw && seg_top > cl_sw) {
2069 float d = (float)(seg_top - cl_sw) * lift;
2070 /* S vertex: south wall continues eastward if SE neighbor same height */
2071 if (nt_se != seg_top) {
2072 if (d > drop_s) drop_s = d;
2073 }
2074 /* W vertex: south wall continues westward if NW neighbor same height */
2075 if (nt_nw != seg_top) {
2076 if (d > drop_w) drop_w = d;
2077 }
2078 }
2079 /* NW edge (W,N): only W vertex (N is never drawn) */
2080 if (edges.draw_nw && seg_top > cl_nw) {
2081 /* W vertex: south wall continues if SW neighbor same height */
2082 if (nt_sw != seg_top) {
2083 float d = (float)(seg_top - cl_nw) * lift;
2084 if (d > drop_w) drop_w = d;
2085 }
2086 }
2087
2088 if (drop_e > 0)
2089 al_draw_line(vx[1] - jx, vy[1] - jy,
2090 vx[1] - jx, vy[1] + drop_e - jy,
2091 border_col, thickness);
2092 if (drop_s > 0)
2093 al_draw_line(vx[2] - jx, vy[2] - jy,
2094 vx[2] - jx, vy[2] + drop_s - jy,
2095 border_col, thickness);
2096 if (drop_w > 0)
2097 al_draw_line(vx[3] - jx, vy[3] - jy,
2098 vx[3] - jx, vy[3] + drop_w - jy,
2099 border_col, thickness);
2100 }
2101
2102 /* Underside face border (floating segments only).
2103 * The underside diamond is at seg_bottom, offset below the top face
2104 * by (seg_top - seg_bottom) * lift pixels. Only drawn if the
2105 * underside face itself was rendered (has_underside_face).
2106 * Only SE and SW edges, NW and NE are always hidden behind the
2107 * segment's own side walls from the camera angle. */
2108 if (has_underside_face && edges.draw_underside) {
2109 float dy = (float)(seg_top - seg_bottom) * lift;
2110 /* SE edge */
2111 al_draw_line(vx[1] - jx, vy[1] + dy - jy,
2112 vx[2] - jx, vy[2] + dy - jy,
2113 border_col, thickness);
2114 /* SW edge */
2115 al_draw_line(vx[2] - jx, vy[2] + dy - jy,
2116 vx[3] - jx, vy[3] + dy - jy,
2117 border_col, thickness);
2118 }
2119}
2120
2140void iso_map_draw(const ISO_MAP* map,
2141 ALLEGRO_BITMAP** tile_bitmaps,
2142 ALLEGRO_BITMAP*** transition_tiles,
2143 int num_masks,
2144 ALLEGRO_BITMAP** overlay_bitmaps,
2145 int num_overlay_tiles,
2146 float cam_px,
2147 float cam_py,
2148 float zoom,
2149 int screen_w,
2150 int screen_h,
2151 int player_mode,
2152 N_ISO_OBJECT* objects,
2153 int num_objects) {
2154 __n_assert(map, return);
2155 __n_assert(tile_bitmaps, return);
2156
2157 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2158
2159 float hw0 = map->proj.half_w;
2160 float hh0 = map->proj.half_h;
2161 float tl0 = map->proj.tile_lift;
2162 float phw = hw0 * zoom;
2163 float phh = hh0 * zoom;
2164 float lift = tl0 * zoom;
2165 float tile_draw_w = 2.0f * phw;
2166 float tile_draw_h = 2.0f * phh;
2167
2168 /* cam_px / cam_py are pre-computed pixel offsets provided by the caller.
2169 * The caller may pass either integer-snapped (`floorf(cam_x * zoom)`) or
2170 * sub-pixel (`cam_x * zoom`) values. Sub-pixel is preferred when paired
2171 * with MIN_LINEAR bilinear sampling for smooth camera motion. Integer
2172 * snap pairs with NEAREST sampling for crisp pixel-art but produces
2173 * "world jumps by 1 pixel" stutter during motion. Chunk pixel offsets
2174 * are added on top of whatever value the caller passes: they remain
2175 * integer at zoom = 1 regardless. */
2176
2177 int tile_w = tile_bitmaps[0] ? al_get_bitmap_width(tile_bitmaps[0]) : 64;
2178 int tile_h = tile_bitmaps[0] ? al_get_bitmap_height(tile_bitmaps[0]) : 32;
2179
2180 int num_edge_masks = ISO_NUM_EDGE_MASKS;
2181
2182 double _iso_t_setup = al_get_time();
2183
2184 /* Build segment draw order.
2185 *
2186 * The draw_order is cached on the ISO_MAP and only rebuilt when
2187 * the segment/height layout actually changes (`draw_order_dirty`
2188 * is set by iso_map_set_segments). For a static-terrain scene
2189 * this drops to a zero-work hit per frame. obj_order / obj_sx_arr
2190 * / obj_sy_arr stay file-static because objects' world positions
2191 * change every frame and the buffer just needs to be big enough;
2192 * per-map ownership isn't required. */
2193 int max_draw_entries = map->width * map->height * ISO_MAX_SEGMENTS_PER_TILE;
2194 ISO_MAP* mut_map = (ISO_MAP*)map; /* the cache fields are deliberately writable */
2195 if (mut_map->cached_draw_order_cap < max_draw_entries) {
2196 ISO_DRAW_ENTRY* grown = (ISO_DRAW_ENTRY*)realloc(
2197 mut_map->cached_draw_order,
2198 (size_t)max_draw_entries * sizeof(ISO_DRAW_ENTRY));
2199 if (!grown) return;
2200 mut_map->cached_draw_order = grown;
2201 mut_map->cached_draw_order_cap = max_draw_entries;
2202 mut_map->draw_order_dirty = 1;
2203 }
2204 if (mut_map->draw_order_dirty) {
2206 map, mut_map->cached_draw_order, mut_map->cached_draw_order_cap);
2207 mut_map->draw_order_dirty = 0;
2208 }
2209 ISO_DRAW_ENTRY* draw_order = mut_map->cached_draw_order;
2210 int num_entries = mut_map->cached_draw_order_count;
2211
2212 /* Pre-process objects: compute screen positions and sort by depth.
2213 * Sort key matches segment draw order: (floor(fz), my, mx). */
2214 int nobj = (objects && num_objects > 0) ? num_objects : 0;
2215 static int* obj_order = NULL;
2216 static float* obj_sx_arr = NULL;
2217 static float* obj_sy_arr = NULL;
2218 static int obj_buf_cap = 0;
2219 if (nobj > obj_buf_cap) {
2220 int new_cap = obj_buf_cap > 0 ? obj_buf_cap : 64;
2221 while (new_cap < nobj) new_cap *= 2;
2222 int* g1 = (int*)realloc(obj_order, (size_t)new_cap * sizeof(int));
2223 float* g2 = (float*)realloc(obj_sx_arr, (size_t)new_cap * sizeof(float));
2224 float* g3 = (float*)realloc(obj_sy_arr, (size_t)new_cap * sizeof(float));
2225 if (!g1 || !g2 || !g3) {
2226 n_log(LOG_ERR, "iso_map_draw: object buffer realloc failed");
2227 /* Keep whatever grew successfully; on next frame the
2228 * remaining one(s) will be retried. */
2229 if (g1) obj_order = g1;
2230 if (g2) obj_sx_arr = g2;
2231 if (g3) obj_sy_arr = g3;
2232 nobj = 0;
2233 } else {
2234 obj_order = g1;
2235 obj_sx_arr = g2;
2236 obj_sy_arr = g3;
2237 obj_buf_cap = new_cap;
2238 }
2239 }
2240
2241 if (nobj > 0) {
2242 for (int i = 0; i < nobj; i++) {
2243 obj_order[i] = i;
2244 objects[i].is_occluded = 0;
2245 objects[i].occlude_clip_y = 1e9f;
2246 float wsx, wsy;
2247 iso_map_to_screen_f(map, objects[i].fx, objects[i].fy, objects[i].fz, &wsx, &wsy);
2248 obj_sx_arr[i] = wsx * zoom + cam_px;
2249 obj_sy_arr[i] = wsy * zoom + cam_py;
2250 }
2251
2252 /* insertion sort by (depth, floor(fz), my, mx) to match segment draw order */
2253 for (int i = 1; i < nobj; i++) {
2254 int key = obj_order[i];
2255 int key_fz = (int)floorf(objects[key].fz);
2256 int key_my = (int)floorf(objects[key].fy);
2257 int key_mx = (int)floorf(objects[key].fx);
2258 int key_depth = key_my + key_mx;
2259 int j = i - 1;
2260 while (j >= 0) {
2261 int cj = obj_order[j];
2262 int cj_fz = (int)floorf(objects[cj].fz);
2263 int cj_my = (int)floorf(objects[cj].fy);
2264 int cj_mx = (int)floorf(objects[cj].fx);
2265 int cj_depth = cj_my + cj_mx;
2266 if (cj_depth > key_depth ||
2267 (cj_depth == key_depth && cj_fz > key_fz) ||
2268 (cj_depth == key_depth && cj_fz == key_fz && cj_my > key_my) ||
2269 (cj_depth == key_depth && cj_fz == key_fz && cj_my == key_my && cj_mx > key_mx)) {
2270 obj_order[j + 1] = obj_order[j];
2271 j--;
2272 } else {
2273 break;
2274 }
2275 }
2276 obj_order[j + 1] = key;
2277 }
2278 }
2279 n_iso_setup_us += (al_get_time() - _iso_t_setup) * 1.0e6;
2280 double _iso_t_pass1 = al_get_time();
2281 /* PASS 1: Ground segments only (bottom == 0), no objects
2282 * Draw all ground-level tiles first so they can never overdraw
2283 * elevated content (entities on bridges, floating segments, etc.).
2284 *
2285 * Wrap the entire pass in al_hold_bitmap_drawing. The previous
2286 * frame-wide wrap regressed because Pass 2 interleaves tile draws
2287 * with object-callback sprite draws (each from a different source
2288 * bitmap), forcing flushes. Pass 1 has no object callbacks, it's
2289 * pure tile draws from whatever source the caller provided. When
2290 * the caller groups tiles + transitions + overlays into one atlas,
2291 * consecutive draws share a source texture and accumulate cleanly
2292 * between state-change flushes. The internal al_set_blender calls
2293 * still flush, but runs of same-blender draws within a blender
2294 * regime now batch.
2295 *
2296 * `blender_is_one` tracks whether the current blender is
2297 * ADD/ONE/INV_ALPHA. Every set to ONE flips it true; every
2298 * set to ALPHA (walls, overlay, borders, hover, grid) flips it
2299 * false. This lets us skip the per-tile "reset to ONE" at the
2300 * head of each iteration when no ALPHA block ran in the
2301 * previous iteration, the common case for uninstrumented
2302 * ground tiles. */
2303 int blender_is_one = 1; /* set by line 2582 above the pass */
2304 /* al_hold_bitmap_drawing wrap around PASS 1, opt-in via
2305 * NILOREA_PASS1_HOLD=1. Wrapping the pass batches consecutive
2306 * bitmap draws (~2 ms speedup) but historically introduced
2307 * intermittent darker tile-mask diamond edges at the boundaries.
2308 * After the bitmap/primitive interleave carve-out (see below)
2309 * the wrap is correct end-to-end, but it remains opt-in by
2310 * environment variable for A/B perf comparison. */
2311 static int s_hold_pass1 = -1;
2312 static int s_hold_per_tile = -1;
2313 static int s_no_transitions = -1;
2314 static int s_no_overlay = -1;
2315 static int s_no_height_borders = -1;
2316 static int s_no_hover_grid = -1;
2317 static int s_tile_limit = -1;
2318 static int s_force_alpha = -1;
2319 if (s_hold_pass1 < 0) {
2320 /* Default is ON; set NILOREA_PASS1_HOLD=0 to opt OUT. */
2321 const char* e = getenv("NILOREA_PASS1_HOLD");
2322 s_hold_pass1 = (e && e[0] == '0') ? 0 : 1;
2323 }
2324 /* PASS 1 instrumentation knobs. Each is env-gated, default-off so
2325 * production builds are unchanged. Useful when investigating
2326 * rendering artefacts.
2327 *
2328 * NILOREA_PASS1_HOLD_PER_TILE=1
2329 * Replace the pass-wide al_hold_bitmap_drawing(true/false)
2330 * wrap with a per-tile cycle. Each tile becomes its own
2331 * isolated batch.
2332 *
2333 * NILOREA_PASS1_NO_TRANSITIONS=1
2334 * Skip the terrain-transitions loop entirely.
2335 *
2336 * NILOREA_PASS1_NO_OVERLAY=1
2337 * Skip the overlay-bitmap draw (ALPHA blender).
2338 *
2339 * NILOREA_PASS1_NO_HEIGHT_BORDERS=1
2340 * Skip the height-borders ALPHA draw.
2341 *
2342 * NILOREA_PASS1_NO_HOVER_GRID=1
2343 * Skip the hover-highlight and grid-overlay ALPHA draws.
2344 *
2345 * NILOREA_PASS1_TILE_LIMIT=N
2346 * Render at most N tiles in PASS 1, then break.
2347 *
2348 * NILOREA_PASS1_FORCE_ALPHA=1
2349 * Force ALPHA blender (instead of ONE) for the base-tile
2350 * and transitions draws. */
2351 if (s_hold_per_tile < 0) {
2352 const char* e = getenv("NILOREA_PASS1_HOLD_PER_TILE");
2353 s_hold_per_tile = (e && e[0] == '1') ? 1 : 0;
2354 }
2355 if (s_no_transitions < 0) {
2356 const char* e = getenv("NILOREA_PASS1_NO_TRANSITIONS");
2357 s_no_transitions = (e && e[0] == '1') ? 1 : 0;
2358 }
2359 if (s_no_overlay < 0) {
2360 const char* e = getenv("NILOREA_PASS1_NO_OVERLAY");
2361 s_no_overlay = (e && e[0] == '1') ? 1 : 0;
2362 }
2363 if (s_no_height_borders < 0) {
2364 const char* e = getenv("NILOREA_PASS1_NO_HEIGHT_BORDERS");
2365 s_no_height_borders = (e && e[0] == '1') ? 1 : 0;
2366 }
2367 if (s_no_hover_grid < 0) {
2368 const char* e = getenv("NILOREA_PASS1_NO_HOVER_GRID");
2369 s_no_hover_grid = (e && e[0] == '1') ? 1 : 0;
2370 }
2371 if (s_tile_limit < 0) {
2372 const char* e = getenv("NILOREA_PASS1_TILE_LIMIT");
2373 s_tile_limit = (e && e[0] >= '0' && e[0] <= '9') ? atoi(e) : 0;
2374 }
2375 if (s_force_alpha < 0) {
2376 const char* e = getenv("NILOREA_PASS1_FORCE_ALPHA");
2377 s_force_alpha = (e && e[0] == '1') ? 1 : 0;
2378 }
2379 /* If per-tile hold is requested, force the pass-wide hold OFF:
2380 * the per-tile cycle below opens/closes its own holds. */
2381 int effective_hold_pass = s_hold_pass1 && !s_hold_per_tile;
2382 if (effective_hold_pass) {
2383 al_hold_bitmap_drawing(true);
2384 /* Batch warmup. The first batched draw after
2385 * al_hold_bitmap_drawing(true) can hit a slightly different
2386 * fragment-shading path than subsequent draws within the same
2387 * held context, producing darker fractional-alpha tile-mask
2388 * edges on the early painter-order tiles.
2389 *
2390 * Defence: prepend a fully-transparent 1-pixel draw at an
2391 * offscreen position so the suspect "first draw in hold
2392 * cycle" is absorbed into an invisible pixel. The actual
2393 * tile draws then all run from the second-draw-onwards
2394 * state, which is the consistent one. Zero visual cost
2395 * (transparent + offscreen) and negligible perf cost. */
2396 if (tile_bitmaps[0]) {
2397 al_draw_tinted_scaled_bitmap(tile_bitmaps[0],
2398 al_map_rgba(0, 0, 0, 0),
2399 0, 0, 1, 1,
2400 -100, -100, 1, 1, 0);
2401 }
2402 }
2403 int _p1_tiles_drawn = 0;
2404 for (int di = 0; di < num_entries; di++) {
2405 if (draw_order[di].bottom > 0) continue; /* skip elevated */
2406
2407 /* Tile limit knob: stop after N ground tiles drawn. */
2408 if (s_tile_limit > 0 && _p1_tiles_drawn >= s_tile_limit) break;
2409 _p1_tiles_drawn++;
2410
2411 /* Per-tile hold cycle: each tile is its own isolated batch.
2412 * The pass-wide hold is suppressed when this knob is on. */
2413 if (s_hold_per_tile) al_hold_bitmap_drawing(true);
2414
2415 int mx = draw_order[di].mx;
2416 int my = draw_order[di].my;
2417 int seg_top = draw_order[di].top;
2418 int h = seg_top;
2419 int base = iso_map_get_terrain(map, mx, my);
2420
2421 /* `al_hold_bitmap_drawing` only batches bitmap draws.
2422 * `al_draw_line` / `al_draw_filled_polygon` (primitives addon)
2423 * draw IMMEDIATELY regardless of hold. So under the pass-wide
2424 * hold:
2425 * - tile base bitmap is queued in batch
2426 * - wall polygon is drawn now at the screen position
2427 * - border line is drawn now
2428 * - more tiles ...
2429 * - end of pass: hold(false) flushes all queued bitmaps
2430 *
2431 * The bitmap flush at end draws on TOP of the primitives,
2432 * hiding height borders / wall edges. Adjacent tiles' diamond
2433 * edges (where transitions + border lines render at the same
2434 * sub-pixel position with bilinear vs line-AA coverage) also
2435 * produce a doubled-outline effect during camera motion.
2436 *
2437 * Fix: for tiles that need primitive draws (elevated walls,
2438 * height borders, hover highlight, grid overlay), step OUT of
2439 * the held batch: flush the queued bitmaps, draw the tile
2440 * fully in immediate mode (bitmaps + primitives interleaved
2441 * correctly), then re-enter the held batch for the next
2442 * batchable tile. The bulk of tiles (flat ground, no overlay,
2443 * no hover, no grid) stays inside the held batch unchanged. */
2444 /* Smooth-mode slope detection, computed HERE, before the hold-batch
2445 * decision below, because a sloped tile's top is drawn with
2446 * al_draw_prim (a primitives-addon call) which renders INCORRECTLY
2447 * inside al_hold_bitmap_drawing(true): the queued-batch state scales
2448 * + misplaces the warped quad (the "flat<->slope transition tiles black
2449 * out / shrink toward screen centre" bug). Such tiles must step out
2450 * of the held batch exactly like wall/hover/grid tiles already do. A
2451 * FLAT tile takes the batch-safe al_draw_tinted_scaled_bitmap path and
2452 * stays held. h_n/h_e/h_s/h_w are reused by the vx/vy block below so
2453 * iso_map_smooth_corner_heights is called only once per tile. */
2454 float h_n = (float)h, h_e = (float)h, h_s = (float)h, h_w = (float)h;
2455 int tile_is_flat = 1;
2456 if (map->smooth_height) {
2457 iso_map_smooth_corner_heights(map, mx, my, &h_n, &h_e, &h_s, &h_w);
2458 tile_is_flat = (h_n == h_e && h_e == h_s && h_s == h_w);
2459 }
2460 int tile_needs_primitives = (seg_top > 0) ||
2461 (map->smooth_height && !tile_is_flat) ||
2462 (!s_no_hover_grid && mx == map->hover_mx && my == map->hover_my) ||
2463 (map->show_grid && !s_no_hover_grid);
2464 int unbatched_tile = effective_hold_pass && tile_needs_primitives;
2465 if (unbatched_tile) {
2466 al_hold_bitmap_drawing(false);
2467 }
2468
2469 /* Per-segment tile overrides: upper_tile for top face, lower_tile for walls */
2470 int top_terrain = base;
2471 int wall_terrain = base;
2472 {
2473 int si = draw_order[di].seg_idx;
2474 int tidx = my * map->width + mx;
2475 if (map->segments && tidx >= 0 && tidx < map->width * map->height &&
2476 si >= 0 && si < map->segments[tidx].count) {
2477 int ut = map->segments[tidx].segs[si].upper_tile;
2478 int lt = map->segments[tidx].segs[si].lower_tile;
2479 if (ut >= 0 && ut < map->num_terrains) top_terrain = ut;
2480 if (lt >= 0 && lt < map->num_terrains) wall_terrain = lt;
2481 }
2482 }
2483
2484 float vx[4], vy[4];
2485 float base_dx, base_dy;
2486
2487 if (map->smooth_height) {
2488 /* h_n/h_e/h_s/h_w + tile_is_flat already computed above. */
2489 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, h_n, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2490 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, h_e, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2491 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, h_s, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2492 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, h_w, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2493 base_dx = vx[3];
2494 base_dy = vy[0];
2495 } else {
2496 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, (float)h, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2497 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2498 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2499 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2500 base_dx = vx[3];
2501 base_dy = vy[0];
2502 }
2503
2504 /* Culling */
2505 float min_vy = fminf(fminf(vy[0], vy[1]), fminf(vy[2], vy[3]));
2506 float max_vy = fmaxf(fmaxf(vy[0], vy[1]), fmaxf(vy[2], vy[3]));
2507 float max_side = fmaxf((float)h, (float)map->max_height) * lift;
2508 if (vx[1] < 0 || vx[3] > (float)screen_w) continue;
2509 if (max_vy + max_side < 0 || min_vy > (float)screen_h) continue;
2510
2511 /* Height-based tint + ambient + dynamic lighting for this segment */
2512 float amb_r = map->ambient_r > 0.0f ? map->ambient_r : 1.0f;
2513 float amb_g = map->ambient_g > 0.0f ? map->ambient_g : 1.0f;
2514 float amb_b = map->ambient_b > 0.0f ? map->ambient_b : 1.0f;
2515 float dyn_r = 0.0f, dyn_g = 0.0f, dyn_b = 0.0f;
2516 if (map->dynamic_light_map) {
2517 int lidx = (my * map->width + mx) * 3;
2518 dyn_r = map->dynamic_light_map[lidx];
2519 dyn_g = map->dynamic_light_map[lidx + 1];
2520 dyn_b = map->dynamic_light_map[lidx + 2];
2521 }
2522 ALLEGRO_COLOR seg_tint = _compute_tile_tint(seg_top, map->max_height,
2524 amb_r, amb_g, amb_b,
2525 dyn_r, dyn_g, dyn_b);
2526 float seg_brightness = 1.0f;
2527 if (map->height_tint_intensity > 0.0f && map->max_height > 0) {
2528 float ratio = (float)seg_top / (float)map->max_height;
2529 seg_brightness = 1.0f - map->height_tint_intensity * (1.0f - ratio);
2530 }
2531 /* Factor in ambient for wall brightness */
2532 seg_brightness *= (amb_r + amb_g + amb_b) / 3.0f;
2533 float wall_dyn = (dyn_r + dyn_g + dyn_b) / 3.0f;
2534 seg_brightness += wall_dyn;
2535 if (seg_brightness > 1.0f) seg_brightness = 1.0f;
2536
2537 /* Top surface: ALWAYS set the blender at the start of every
2538 * tile, dropping any cross-tile cache. Under
2539 * `al_hold_bitmap_drawing(true)`, `al_set_blender` flushes the
2540 * current held batch and starts a new one. If the call were
2541 * skipped when the previous tile ended on ONE, the held
2542 * batch's flush boundaries would depend on which tiles had
2543 * overlays/walls/hover/grid, and that set shifts when the
2544 * camera moves. Inconsistent flush boundaries cause
2545 * inconsistent GPU-rasterised pixel coverage on shared
2546 * diamond edges, producing flickering darker borders during
2547 * camera motion. Forcing the call here makes the flush
2548 * boundary deterministic at every tile, trading O(1000) extra
2549 * al_set_blender calls per frame for stable rasterisation.
2550 * The within-tile transitions loop keeps its conditional `if
2551 * (!blender_is_one)` skip because within a tile the state is
2552 * fresh and skipping is safe. */
2553 if (s_force_alpha) {
2554 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2555 blender_is_one = 0;
2556 } else {
2557 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2558 blender_is_one = 1;
2559 }
2560 if (map->smooth_height && !tile_is_flat) {
2561 _draw_tile_warped(tile_bitmaps[top_terrain], vx, vy, tile_w, tile_h, seg_tint);
2562 } else {
2563 /* Flat tile (or CUT mode): the al_draw_prim warp samples atlas
2564 * sub-bitmaps differently from al_draw_tinted_scaled_bitmap, which
2565 * renders flat ground tiles wrong once tiles are packed into the
2566 * terrain atlas. For a flat tile the warp geometry is identical to
2567 * this scaled-bitmap draw anyway (verified: corner span == tdw*tdh),
2568 * so route flat tiles through the proven atlas-correct path and
2569 * reserve the warp for genuinely sloped tiles. */
2570 al_draw_tinted_scaled_bitmap(tile_bitmaps[top_terrain], seg_tint,
2571 0, 0, (float)tile_w, (float)tile_h,
2572 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2573 }
2574
2575 /* Cliff walls (internally sets ONE blender). */
2576 if (seg_top > 0) {
2577 if (map->smooth_height) {
2578 int h_east = iso_map_get_height(map, mx + 1, my);
2579 int h_south = iso_map_get_height(map, mx, my + 1);
2580 int h_se = iso_map_get_height(map, mx + 1, my + 1);
2581 if ((h - h_east > map->smooth_slope_max) ||
2582 (h - h_south > map->smooth_slope_max) ||
2583 (h - h_se > map->smooth_slope_max)) {
2584 float ce_x, ce_y, cs_x, cs_y, cw_x, cw_y;
2585 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &ce_x, &ce_y);
2586 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &cs_x, &cs_y);
2587 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &cw_x, &cw_y);
2588 _draw_segment_sides(map, ce_x, ce_y, cs_x, cs_y, cw_x, cw_y,
2589 mx, my, 0, seg_top, lift, seg_brightness, wall_terrain);
2590 /* blender_is_one stays 1: function leaves state on ONE */
2591 }
2592 } else {
2593 _draw_segment_sides(map, vx[1], vy[1], vx[2], vy[2], vx[3], vy[3],
2594 mx, my, 0, seg_top, lift, seg_brightness, wall_terrain);
2595 /* blender_is_one stays 1 */
2596 }
2597 }
2598
2599 /* Terrain transitions (same tint as base tile; ONE blender) */
2600 if (transition_tiles && !s_no_transitions) {
2601 int edge_bits_arr[16];
2602 int corner_bits_arr[16];
2603 int nt = map->num_terrains < 16 ? map->num_terrains : 16;
2604 memset(edge_bits_arr, 0, sizeof(int) * (size_t)nt);
2605 memset(corner_bits_arr, 0, sizeof(int) * (size_t)nt);
2606 iso_map_calc_transitions_full(map, mx, my, edge_bits_arr, corner_bits_arr);
2607
2608 for (int t = base + 1; t < nt; t++) {
2609 if (edge_bits_arr[t] != 0 && transition_tiles[t] &&
2610 transition_tiles[t][edge_bits_arr[t]] != NULL) {
2611 if (!blender_is_one && !s_force_alpha) {
2612 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2613 blender_is_one = 1;
2614 }
2615 if (map->smooth_height && !tile_is_flat) {
2616 _draw_tile_warped(transition_tiles[t][edge_bits_arr[t]],
2617 vx, vy, tile_w, tile_h, seg_tint);
2618 } else {
2619 al_draw_tinted_scaled_bitmap(transition_tiles[t][edge_bits_arr[t]],
2620 seg_tint,
2621 0, 0, (float)tile_w, (float)tile_h,
2622 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2623 }
2624 }
2625
2626 int cidx = num_edge_masks + corner_bits_arr[t];
2627 if (corner_bits_arr[t] != 0 && cidx < num_masks &&
2628 transition_tiles[t] && transition_tiles[t][cidx] != NULL) {
2629 if (!blender_is_one && !s_force_alpha) {
2630 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2631 blender_is_one = 1;
2632 }
2633 if (map->smooth_height && !tile_is_flat) {
2635 vx, vy, tile_w, tile_h, seg_tint);
2636 } else {
2637 al_draw_tinted_scaled_bitmap(transition_tiles[t][cidx],
2638 seg_tint,
2639 0, 0, (float)tile_w, (float)tile_h,
2640 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2641 }
2642 }
2643 }
2644 }
2645
2646 /* Overlay tile (drawn after terrain transitions, before
2647 * walls/borders). Overlay bitmaps are premultiplied (loaded
2648 * through the same path as base tiles + transitions), so
2649 * ALPHA blending would produce quadratic darkening at
2650 * fractional-alpha edges. Use ONE/INVERSE_ALPHA which matches
2651 * the premul source format. Also avoids cross-tile
2652 * contamination under al_hold_bitmap_drawing(true), where
2653 * al_set_blender does not flush the held batch mid-pass: an
2654 * ALPHA blender set here would otherwise apply to draws
2655 * emitted earlier in the batch by other tiles when the batch
2656 * eventually flushes. */
2657 if (overlay_bitmaps && map->overlay && num_overlay_tiles > 0 && !s_no_overlay) {
2658 int ov_idx = my * map->width + mx;
2659 int ov_tile = map->overlay[ov_idx];
2660 if (ov_tile > 0 && ov_tile < num_overlay_tiles && overlay_bitmaps[ov_tile]) {
2661 if (!blender_is_one) {
2662 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2663 blender_is_one = 1;
2664 }
2665 if (map->smooth_height && !tile_is_flat) {
2666 _draw_tile_warped(overlay_bitmaps[ov_tile], vx, vy, tile_w, tile_h, seg_tint);
2667 } else {
2668 al_draw_tinted_scaled_bitmap(overlay_bitmaps[ov_tile], seg_tint,
2669 0, 0, (float)tile_w, (float)tile_h,
2670 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2671 }
2672 }
2673 }
2674
2675 /* Height borders (ONE blender; lines use RGB=0 so premul == straight) */
2676 if (seg_top > 0 && !s_no_height_borders) {
2677 if (!blender_is_one) {
2678 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2679 blender_is_one = 1;
2680 }
2681 _draw_height_borders(map, mx, my, draw_order[di].seg_idx,
2682 0, seg_top,
2683 draw_order[di].underside,
2684 vx, vy, lift);
2685 }
2686
2687 /* Hover highlight (ONE blender; colors premultiplied below) */
2688 if (!s_no_hover_grid && mx == map->hover_mx && my == map->hover_my) {
2689 if (!blender_is_one) {
2690 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2691 blender_is_one = 1;
2692 }
2693 /* Premul: each RGB channel scaled by alpha/255. */
2694 ALLEGRO_COLOR hc = player_mode
2695 ? al_map_rgba(100 * 120 / 255, 255 * 120 / 255, 100 * 120 / 255, 120)
2696 : al_map_rgba(255 * 120 / 255, 255 * 120 / 255, 100 * 120 / 255, 120);
2697 float verts[8] = {vx[0], vy[0], vx[1], vy[1],
2698 vx[2], vy[2], vx[3], vy[3]};
2699 al_draw_filled_polygon(verts, 4, hc);
2700 ALLEGRO_COLOR oc = player_mode
2701 ? al_map_rgba(100 * 200 / 255, 255 * 200 / 255, 100 * 200 / 255, 200)
2702 : al_map_rgba(255 * 200 / 255, 255 * 200 / 255, 100 * 200 / 255, 200);
2703 al_draw_line(vx[0], vy[0], vx[1], vy[1], oc, 1.5f);
2704 al_draw_line(vx[1], vy[1], vx[2], vy[2], oc, 1.5f);
2705 al_draw_line(vx[2], vy[2], vx[3], vy[3], oc, 1.5f);
2706 al_draw_line(vx[3], vy[3], vx[0], vy[0], oc, 1.5f);
2707 }
2708
2709 /* Grid overlay (ONE blender; colors premultiplied below).
2710 * No blender_is_one = 1 update needed: this is the last block in
2711 * the iteration that touches the blender, and the next iteration
2712 * unconditionally re-sets the blender at the top of the loop. */
2713 if (map->show_grid && !s_no_hover_grid) {
2714 if (!blender_is_one) {
2715 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2716 }
2717 /* Premul: white 60/255 alpha is (60,60,60,60). */
2718 ALLEGRO_COLOR gc = al_map_rgba(60, 60, 60, 60);
2719 al_draw_line(vx[0], vy[0], vx[1], vy[1], gc, 1.0f);
2720 al_draw_line(vx[1], vy[1], vx[2], vy[2], gc, 1.0f);
2721 al_draw_line(vx[2], vy[2], vx[3], vy[3], gc, 1.0f);
2722 al_draw_line(vx[3], vy[3], vx[0], vy[0], gc, 1.0f);
2723
2724 if (h > 0) {
2725 unsigned char a2 = (unsigned char)(60 + h * 35);
2726 /* Premul: (R*a/255, G*a/255, B*a/255, a) for (255,200,100,a). */
2727 ALLEGRO_COLOR hc2 = al_map_rgba(a2,
2728 (unsigned char)(200 * a2 / 255),
2729 (unsigned char)(100 * a2 / 255),
2730 a2);
2731 float dot_x = (vx[0] + vx[2]) / 2.0f;
2732 float dot_y = (vy[0] + vy[1] + vy[2] + vy[3]) / 4.0f;
2733 al_draw_filled_circle(dot_x, dot_y, 2.0f * zoom, hc2);
2734 }
2735 }
2736
2737 /* Per-tile hold cycle: flush this tile's batch before moving
2738 * on. Each tile becomes its own VBO. */
2739 if (s_hold_per_tile) al_hold_bitmap_drawing(false);
2740
2741 /* Re-enter the held batch for the next batchable tile.
2742 * Resets the blender to ONE for the next tile's base bitmap,
2743 * since immediate-mode primitive draws inside the unbatched
2744 * section may have left other state.
2745 * No blender_is_one = 1 update: the next iteration unconditionally
2746 * re-sets the blender at the top of the loop. */
2747 if (unbatched_tile) {
2748 al_hold_bitmap_drawing(true);
2749 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2750 }
2751 }
2752
2753 if (effective_hold_pass) al_hold_bitmap_drawing(false);
2754 n_iso_pass1_us += (al_get_time() - _iso_t_pass1) * 1.0e6;
2755 double _iso_t_pass2 = al_get_time();
2756 /* PASS 2: Elevated segments + ALL objects (interleaved)
2757 * Objects are interleaved at every draw_order entry (ground and
2758 * elevated) to preserve correct depth ordering. Only elevated
2759 * segments (bottom > 0) are actually rendered, ground segments
2760 * were already drawn in Pass 1. */
2761 int obj_cursor = 0;
2762 for (int di = 0; di < num_entries; di++) {
2763 int mx = draw_order[di].mx;
2764 int my = draw_order[di].my;
2765 int seg_bottom = draw_order[di].bottom;
2766
2767 /* Object interleaving runs at every entry regardless of
2768 * ground/elevated, this ensures objects at ground-level depth
2769 * positions are drawn at the correct point in the sort order. */
2770 if (nobj > 0) {
2771 int seg_depth = my + mx;
2772 while (obj_cursor < nobj) {
2773 int oi = obj_order[obj_cursor];
2774 int obj_fz = (int)floorf(objects[oi].fz);
2775 int obj_my = (int)floorf(objects[oi].fy);
2776 int obj_mx = (int)floorf(objects[oi].fx);
2777 int obj_depth = obj_my + obj_mx;
2778 if (obj_depth > seg_depth ||
2779 (obj_depth == seg_depth && obj_fz > seg_bottom) ||
2780 (obj_depth == seg_depth && obj_fz == seg_bottom && obj_my > my) ||
2781 (obj_depth == seg_depth && obj_fz == seg_bottom && obj_my == my && obj_mx >= mx))
2782 break;
2783 if (objects[oi].draw) {
2784 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
2785 zoom, 1.0f, objects[oi].user_data);
2786 }
2787 obj_cursor++;
2788 }
2789 }
2790
2791 /* Ground segments already drawn in Pass 1, skip rendering */
2792 if (seg_bottom == 0) continue;
2793
2794 /* Elevated segment rendering */
2795 int seg_top = draw_order[di].top;
2796 int h = seg_top;
2797 int base = iso_map_get_terrain(map, mx, my);
2798
2799 /* Per-segment tile overrides for elevated segments */
2800 int etop_terrain = base;
2801 int ewall_terrain = base;
2802 {
2803 int si = draw_order[di].seg_idx;
2804 int tidx = my * map->width + mx;
2805 if (map->segments && tidx >= 0 && tidx < map->width * map->height &&
2806 si >= 0 && si < map->segments[tidx].count) {
2807 int ut = map->segments[tidx].segs[si].upper_tile;
2808 int lt = map->segments[tidx].segs[si].lower_tile;
2809 if (ut >= 0 && ut < map->num_terrains) etop_terrain = ut;
2810 if (lt >= 0 && lt < map->num_terrains) ewall_terrain = lt;
2811 }
2812 }
2813
2814 float vx[4], vy[4];
2815 float base_dx, base_dy;
2816 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, (float)h, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2817 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2818 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2819 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2820 base_dx = vx[3];
2821 base_dy = vy[0];
2822
2823 /* Culling, objects already handled above, safe to continue */
2824 float min_vy = fminf(fminf(vy[0], vy[1]), fminf(vy[2], vy[3]));
2825 float max_vy = fmaxf(fmaxf(vy[0], vy[1]), fmaxf(vy[2], vy[3]));
2826 float max_side = fmaxf((float)h, (float)map->max_height) * lift;
2827 if (vx[1] < 0 || vx[3] > (float)screen_w) continue;
2828 if (max_vy + max_side < 0 || min_vy > (float)screen_h) continue;
2829
2830 /* Underside face for floating segments */
2831 if (draw_order[di].underside && tile_bitmaps[etop_terrain]) {
2832 float under_sx, under_sy;
2833 iso_map_to_screen(map, mx, my, seg_bottom, &under_sx, &under_sy);
2834 float under_dx = under_sx * zoom + cam_px;
2835 float under_dy = under_sy * zoom + cam_py;
2836 _draw_segment_underside(tile_bitmaps[etop_terrain], under_dx, under_dy,
2837 phw, phh, tile_draw_w, tile_draw_h,
2838 tile_w, tile_h);
2839 }
2840
2841 /* Height-based tint for elevated segment */
2842 /* Elevated segment: same ambient + dynamic light from base tile */
2843 float eamb_r = map->ambient_r > 0.0f ? map->ambient_r : 1.0f;
2844 float eamb_g = map->ambient_g > 0.0f ? map->ambient_g : 1.0f;
2845 float eamb_b = map->ambient_b > 0.0f ? map->ambient_b : 1.0f;
2846 float edyn_r = 0.0f, edyn_g = 0.0f, edyn_b = 0.0f;
2847 if (map->dynamic_light_map) {
2848 int elidx = (my * map->width + mx) * 3;
2849 edyn_r = map->dynamic_light_map[elidx];
2850 edyn_g = map->dynamic_light_map[elidx + 1];
2851 edyn_b = map->dynamic_light_map[elidx + 2];
2852 }
2853 ALLEGRO_COLOR eseg_tint = _compute_tile_tint(seg_top, map->max_height,
2855 eamb_r, eamb_g, eamb_b,
2856 edyn_r, edyn_g, edyn_b);
2857 float eseg_brightness = 1.0f;
2858 if (map->height_tint_intensity > 0.0f && map->max_height > 0) {
2859 float ratio = (float)seg_top / (float)map->max_height;
2860 eseg_brightness = 1.0f - map->height_tint_intensity * (1.0f - ratio);
2861 }
2862 eseg_brightness *= (eamb_r + eamb_g + eamb_b) / 3.0f;
2863 eseg_brightness += (edyn_r + edyn_g + edyn_b) / 3.0f;
2864 if (eseg_brightness > 1.0f) eseg_brightness = 1.0f;
2865
2866 /* Top surface (flat, elevated segments don't use smooth height) */
2867 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2868 al_draw_tinted_scaled_bitmap(tile_bitmaps[etop_terrain], eseg_tint,
2869 0, 0, (float)tile_w, (float)tile_h,
2870 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2871
2872 /* Cliff walls + height borders */
2873 if (seg_top > seg_bottom) {
2874 _draw_segment_sides(map, vx[1], vy[1], vx[2], vy[2], vx[3], vy[3],
2875 mx, my, seg_bottom, seg_top, lift, eseg_brightness, ewall_terrain);
2876 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2877 _draw_height_borders(map, mx, my, draw_order[di].seg_idx,
2878 seg_bottom, seg_top,
2879 draw_order[di].underside,
2880 vx, vy, lift);
2881 }
2882
2883 /* Hover highlight */
2884 if (mx == map->hover_mx && my == map->hover_my) {
2885 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2886 ALLEGRO_COLOR hc = player_mode
2887 ? al_map_rgba(100, 255, 100, 120)
2888 : al_map_rgba(255, 255, 100, 120);
2889 float verts[8] = {vx[0], vy[0], vx[1], vy[1],
2890 vx[2], vy[2], vx[3], vy[3]};
2891 al_draw_filled_polygon(verts, 4, hc);
2892 ALLEGRO_COLOR oc = player_mode
2893 ? al_map_rgba(100, 255, 100, 200)
2894 : al_map_rgba(255, 255, 100, 200);
2895 al_draw_line(vx[0], vy[0], vx[1], vy[1], oc, 1.5f);
2896 al_draw_line(vx[1], vy[1], vx[2], vy[2], oc, 1.5f);
2897 al_draw_line(vx[2], vy[2], vx[3], vy[3], oc, 1.5f);
2898 al_draw_line(vx[3], vy[3], vx[0], vy[0], oc, 1.5f);
2899 }
2900
2901 /* Grid overlay */
2902 if (map->show_grid) {
2903 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2904 ALLEGRO_COLOR gc = al_map_rgba(255, 255, 255, 60);
2905 al_draw_line(vx[0], vy[0], vx[1], vy[1], gc, 1.0f);
2906 al_draw_line(vx[1], vy[1], vx[2], vy[2], gc, 1.0f);
2907 al_draw_line(vx[2], vy[2], vx[3], vy[3], gc, 1.0f);
2908 al_draw_line(vx[3], vy[3], vx[0], vy[0], gc, 1.0f);
2909
2910 if (h > 0) {
2911 ALLEGRO_COLOR hc2 = al_map_rgba(255, 200, 100,
2912 (unsigned char)(60 + h * 35));
2913 float dot_x = (vx[0] + vx[2]) / 2.0f;
2914 float dot_y = (vy[0] + vy[1] + vy[2] + vy[3]) / 4.0f;
2915 al_draw_filled_circle(dot_x, dot_y, 2.0f * zoom, hc2);
2916 }
2917 }
2918 }
2919
2920 /* Draw any remaining objects past the last entry */
2921 while (obj_cursor < nobj) {
2922 int oi = obj_order[obj_cursor];
2923 if (objects[oi].draw) {
2924 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
2925 zoom, 1.0f, objects[oi].user_data);
2926 }
2927 obj_cursor++;
2928 }
2929
2930 n_iso_pass2_us += (al_get_time() - _iso_t_pass2) * 1.0e6;
2931 double _iso_t_pass3 = al_get_time();
2932 /* Step 7: Occlusion detection and clipped overlay pass.
2933 * For each occluded entity, restrict the ghost draw to the region
2934 * below the occluding tile's north vertex using a clipping rectangle. */
2935 for (int i = 0; i < nobj; i++) {
2936 int oi = obj_order[i];
2937 if (objects[oi].occluded_alpha <= 0.0f) continue;
2938 if (!objects[oi].draw) continue;
2939
2940 float clip_y = 1e9f;
2942 objects[oi].fx, objects[oi].fy, objects[oi].fz,
2943 obj_sx_arr[oi], obj_sy_arr[oi],
2944 objects[oi].sprite_h,
2945 objects[oi].sprite_half_w,
2946 cam_px, cam_py, zoom,
2947 &clip_y))
2948 continue;
2949
2950 objects[oi].is_occluded = 1;
2951 objects[oi].occlude_clip_y = clip_y;
2952
2953 /* Clip ghost overlay to below the occluder's top edge. The
2954 * ghost is drawn at alpha < 1 to make the occluded sprite
2955 * look semi-transparent. Use straight-alpha blending here:
2956 * with the premultiplied (ADD/ONE/INV_ALPHA) blender the
2957 * 0.4-alpha overlay is ADDED on top of the already-drawn
2958 * full-bright sprite, brightening it instead of revealing
2959 * what's behind. ALPHA/INV_ALPHA does the standard
2960 * src*alpha + dst*(1-alpha) transparent compositing the
2961 * caller meant. */
2962 int prev_cx, prev_cy, prev_cw, prev_ch;
2963 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
2964 int clip_top = (int)floorf(clip_y);
2965 if (clip_top < prev_cy) clip_top = prev_cy;
2966 al_set_clipping_rectangle(prev_cx, clip_top,
2967 prev_cw, prev_cy + prev_ch - clip_top);
2968 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2969 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
2970 zoom, objects[oi].occluded_alpha, objects[oi].user_data);
2971 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
2972 }
2973
2974 n_iso_pass3_us += (al_get_time() - _iso_t_pass3) * 1.0e6;
2975
2976 /* The draw_order / obj_* buffers are file-static and reused
2977 * across calls; no per-call free. They survive for the process
2978 * lifetime which is fine: max draw entries is bounded by
2979 * map->width * map->height * ISO_MAX_SEGMENTS_PER_TILE (~2 KB
2980 * for typical 32x32x2 chunks), and obj_buf_cap grows once to the
2981 * scene's peak entity count. */
2982} /* iso_map_draw() */
2983
2984/* Transition mask & tile generation (Article 934) */
2985
2993void iso_mask_tile_to_diamond(ALLEGRO_BITMAP* bmp, int tile_w, int tile_h) {
2994 __n_assert(bmp, return);
2995
2996 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(bmp,
2997 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_READWRITE);
2998 if (!lr) return;
2999
3000 for (int py = 0; py < tile_h; py++) {
3001 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3002 for (int px = 0; px < tile_w; px++) {
3003 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3004 row[px * 4 + 0] = 0;
3005 row[px * 4 + 1] = 0;
3006 row[px * 4 + 2] = 0;
3007 row[px * 4 + 3] = 0;
3008 }
3009 }
3010 }
3011 al_unlock_bitmap(bmp);
3012}
3013
3024void iso_generate_transition_masks(ALLEGRO_BITMAP** masks, int tile_w, int tile_h) {
3025 __n_assert(masks, return);
3026
3027 float cx = (float)tile_w / 2.0f;
3028 float cy = (float)tile_h / 2.0f;
3029
3030 ALLEGRO_STATE state;
3031 al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
3032
3033 /* Edge masks (0..15) */
3034 for (int mask = 0; mask < ISO_NUM_EDGE_MASKS; mask++) {
3035 masks[mask] = al_create_bitmap(tile_w, tile_h);
3036 al_set_target_bitmap(masks[mask]);
3037 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3038
3039 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(masks[mask],
3040 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_WRITEONLY);
3041 if (!lr) continue;
3042
3043 for (int py = 0; py < tile_h; py++) {
3044 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3045 for (int px = 0; px < tile_w; px++) {
3046 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3047 row[px * 4 + 0] = 0;
3048 row[px * 4 + 1] = 0;
3049 row[px * 4 + 2] = 0;
3050 row[px * 4 + 3] = 0;
3051 continue;
3052 }
3053
3054 float alpha = 0.0f;
3055 float nx = ((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
3056 float ny = ((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
3057 float gw = 0.65f;
3058
3059 if (mask & ISO_EDGE_W) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (-nx - ny) / gw)));
3060 if (mask & ISO_EDGE_N) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (nx - ny) / gw)));
3061 if (mask & ISO_EDGE_E) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (nx + ny) / gw)));
3062 if (mask & ISO_EDGE_S) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (-nx + ny) / gw)));
3063
3064 if (alpha > 1.0f) alpha = 1.0f;
3065 alpha = alpha * alpha * (3.0f - 2.0f * alpha);
3066
3067 unsigned char a = (unsigned char)(alpha * 255.0f);
3068 row[px * 4 + 0] = a;
3069 row[px * 4 + 1] = a;
3070 row[px * 4 + 2] = a;
3071 row[px * 4 + 3] = a;
3072 }
3073 }
3074 al_unlock_bitmap(masks[mask]);
3075 }
3076
3077 /* Corner masks (16..31)
3078 * In isometric diamond projection, diagonal neighbors map to diamond tips:
3079 * NW (mx-1,my-1) = North tip (0, -1) in normalized coords
3080 * NE (mx+1,my-1) = East tip (1, 0)
3081 * SE (mx+1,my+1) = South tip (0, 1)
3082 * SW (mx-1,my+1) = West tip (-1, 0) */
3083 for (int mask = 0; mask < ISO_NUM_CORNER_MASKS; mask++) {
3084 int idx = ISO_NUM_EDGE_MASKS + mask;
3085 masks[idx] = al_create_bitmap(tile_w, tile_h);
3086 al_set_target_bitmap(masks[idx]);
3087 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3088
3089 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(masks[idx],
3090 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_WRITEONLY);
3091 if (!lr) continue;
3092
3093 for (int py = 0; py < tile_h; py++) {
3094 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3095 for (int px = 0; px < tile_w; px++) {
3096 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3097 row[px * 4 + 0] = 0;
3098 row[px * 4 + 1] = 0;
3099 row[px * 4 + 2] = 0;
3100 row[px * 4 + 3] = 0;
3101 continue;
3102 }
3103
3104 float alpha = 0.0f;
3105 float nx = ((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
3106 float ny = ((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
3107 float corner_radius = 0.55f;
3108
3109 if (mask & ISO_CORNER_NW) {
3110 float dy = ny + 1.0f;
3111 float dist = sqrtf(nx * nx + dy * dy);
3112 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3113 alpha = fmaxf(alpha, c_alpha);
3114 }
3115 if (mask & ISO_CORNER_NE) {
3116 float dx = nx - 1.0f;
3117 float dist = sqrtf(dx * dx + ny * ny);
3118 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3119 alpha = fmaxf(alpha, c_alpha);
3120 }
3121 if (mask & ISO_CORNER_SE) {
3122 float dy = ny - 1.0f;
3123 float dist = sqrtf(nx * nx + dy * dy);
3124 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3125 alpha = fmaxf(alpha, c_alpha);
3126 }
3127 if (mask & ISO_CORNER_SW) {
3128 float dx = nx + 1.0f;
3129 float dist = sqrtf(dx * dx + ny * ny);
3130 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3131 alpha = fmaxf(alpha, c_alpha);
3132 }
3133
3134 if (alpha > 1.0f) alpha = 1.0f;
3135 alpha = alpha * alpha * (3.0f - 2.0f * alpha);
3136
3137 unsigned char a = (unsigned char)(alpha * 255.0f);
3138 row[px * 4 + 0] = a;
3139 row[px * 4 + 1] = a;
3140 row[px * 4 + 2] = a;
3141 row[px * 4 + 3] = a;
3142 }
3143 }
3144 al_unlock_bitmap(masks[idx]);
3145 }
3146
3147 al_restore_state(&state);
3148 n_log(LOG_INFO, "Generated %d transition masks (%dx%d)", ISO_NUM_MASKS, tile_w, tile_h);
3149}
3150
3155void iso_generate_transition_tiles(ALLEGRO_BITMAP*** tiles,
3156 ALLEGRO_BITMAP** masks,
3157 ALLEGRO_BITMAP** tile_bitmaps,
3158 int num_terrains,
3159 int tile_w,
3160 int tile_h) {
3161 __n_assert(tiles, return);
3162 __n_assert(masks, return);
3163 __n_assert(tile_bitmaps, return);
3164
3165 ALLEGRO_STATE state;
3166 al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
3167
3168 for (int t = 0; t < num_terrains; t++) {
3169 for (int m = 0; m < ISO_NUM_MASKS; m++) {
3170 /* Skip mask 0 (no edges active) and mask 16 (no corners active) */
3171 if ((m < ISO_NUM_EDGE_MASKS && m == 0) ||
3172 (m >= ISO_NUM_EDGE_MASKS && m == ISO_NUM_EDGE_MASKS)) {
3173 tiles[t][m] = NULL;
3174 continue;
3175 }
3176
3177 tiles[t][m] = al_create_bitmap(tile_w, tile_h);
3178 al_set_target_bitmap(tiles[t][m]);
3179 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3180
3181 /* Draw the terrain tile first */
3182 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
3183 al_draw_bitmap(tile_bitmaps[t], 0, 0, 0);
3184
3185 /* Multiply by the alpha mask: keeps terrain color
3186 * but applies the mask's alpha for the transition gradient */
3187 al_set_blender(ALLEGRO_ADD, ALLEGRO_DEST_COLOR, ALLEGRO_ZERO);
3188 al_draw_bitmap(masks[m], 0, 0, 0);
3189 }
3190 }
3191
3192 al_restore_state(&state);
3193 n_log(LOG_INFO, "Pre-composited transition tiles for %d terrains x %d masks", num_terrains, ISO_NUM_MASKS);
3194}
3195
3196#endif /* HAVE_ALLEGRO - iso_map_draw */
3197
3198#ifdef N_ASTAR_H
3208ASTAR_GRID* iso_map_to_astar_grid(const ISO_MAP* map, int max_height_diff, int start_x, int start_y) {
3209 __n_assert(map, return NULL);
3210
3211 ASTAR_GRID* grid = n_astar_grid_new(map->width, map->height, 1);
3212 if (!grid) return NULL;
3213
3214 int total = map->width * map->height;
3215
3216 /* First pass: all cells unwalkable, set costs */
3217 for (int y = 0; y < map->height; y++) {
3218 for (int x = 0; x < map->width; x++) {
3219 n_astar_grid_set_walkable(grid, x, y, 0, 0);
3220 int h = map->heightmap[y * map->width + x];
3221 int cost = ASTAR_COST_CARDINAL + h * 100;
3222 n_astar_grid_set_cost(grid, x, y, 0, cost);
3223 }
3224 }
3225
3226 /* Flood-fill from (start_x, start_y) to mark reachable cells as walkable.
3227 * This ensures height constraints are checked per-edge (between consecutive
3228 * cells) rather than per-cell, so tiles on a plateau edge remain walkable
3229 * when approached from the same elevation. */
3230 if (start_x < 0 || start_x >= map->width || start_y < 0 || start_y >= map->height)
3231 return grid;
3232
3233 int start_ab = map->ability[start_y * map->width + start_x];
3234 if (start_ab != WALK && start_ab != SWIM)
3235 return grid;
3236
3237 int* queue = (int*)malloc((size_t)total * 2 * sizeof(int));
3238 if (!queue) {
3239 n_astar_grid_free(grid);
3240 return NULL;
3241 }
3242
3243 uint8_t* visited = (uint8_t*)calloc((size_t)total, sizeof(uint8_t));
3244 if (!visited) {
3245 free(queue);
3246 n_astar_grid_free(grid);
3247 return NULL;
3248 }
3249
3250 int qfront = 0, qback = 0;
3251
3252 /* seed the start cell */
3253 visited[start_y * map->width + start_x] = 1;
3254 n_astar_grid_set_walkable(grid, start_x, start_y, 0, 1);
3255 queue[qback++] = start_x;
3256 queue[qback++] = start_y;
3257
3258 /* 8-directional expansion (matches ASTAR_ALLOW_DIAGONAL) */
3259 const int dirs[][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}};
3260
3261 while (qfront < qback) {
3262 int cx = queue[qfront++];
3263 int cy = queue[qfront++];
3264 int ch = map->heightmap[cy * map->width + cx];
3265
3266 for (int d = 0; d < 8; d++) {
3267 int nx = cx + dirs[d][0];
3268 int ny = cy + dirs[d][1];
3269 if (nx < 0 || nx >= map->width || ny < 0 || ny >= map->height) continue;
3270 if (visited[ny * map->width + nx]) continue;
3271
3272 int nab = map->ability[ny * map->width + nx];
3273 if (nab != WALK && nab != SWIM) continue;
3274
3275 int nh = map->heightmap[ny * map->width + nx];
3276 if (max_height_diff >= 0 && abs(ch - nh) > max_height_diff) continue;
3277
3278 visited[ny * map->width + nx] = 1;
3279 n_astar_grid_set_walkable(grid, nx, ny, 0, 1);
3280 queue[qback++] = nx;
3281 queue[qback++] = ny;
3282 }
3283 }
3284
3285 free(queue);
3286 free(visited);
3287
3288 return grid;
3289}
3290#endif /* N_ASTAR_H */
3291
3292#endif /* #ifndef NOISOENGINE */
static int player_mode
Definition ex_gui.c:62
static void draw(N_GUI_CTX *gui)
draw the whole context into a memory bitmap
static int screen_w
static ALLEGRO_BITMAP * transition_tiles[8][(16+16)]
static ALLEGRO_BITMAP * tile_bitmaps[8]
static int screen_h
static int mode
static const char * row(N_GUI_TREE *tree, int index)
the text of a visible row, "" past the end
static int h(void *ctx, const N_ROUTE_MATCH *m)
a handler that does nothing (the test only routes)
#define M_PI
volatile char key[227]
TRUE while the key is held.
volatile int mouse_x
mouse x in screen coordinates
volatile int mouse_y
mouse y in screen coordinates
void n_astar_grid_set_cost(ASTAR_GRID *grid, int x, int y, int z, int cost)
Set a cell's movement cost multiplier.
Definition n_astar.c:312
void n_astar_grid_free(ASTAR_GRID *grid)
Free a grid and all its internal data.
Definition n_astar.c:271
#define ASTAR_COST_CARDINAL
Default cost for straight movement (fixed-point x1000).
Definition n_astar.h:79
void n_astar_grid_set_walkable(ASTAR_GRID *grid, int x, int y, int z, uint8_t walkable)
Set a cell's walkability.
Definition n_astar.c:286
ASTAR_GRID * n_astar_grid_new(int width, int height, int depth)
Create a new grid for A* pathfinding.
Definition n_astar.c:235
Grid structure holding walkability, costs, and dimensions.
Definition n_astar.h:153
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:204
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:279
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:263
int bottom
segment bottom height (primary sort key)
ISO_TILE_SEGMENT segs[2]
int * neighbor_heights_west
Heights from west neighbor chunk's east edge [height], NULL = boundary.
int lower_tile
terrain index for wall/side faces (-1 = use tile's terrain layer)
int * ability
walkability per cell [height * width] (WALK/SWIM/BLCK)
int height
map height in tiles (Y axis)
int upper_tile
terrain index for top face (-1 = use tile's terrain layer)
int draw_order_dirty
int neighbor_top_se
top height of SE neighbor (0 if absent/boundary)
float tile_lift
vertical pixel offset per height unit
float zoom_min
minimum allowed zoom
int * neighbor_heights_east
Heights from east neighbor chunk's west edge [height], NULL = boundary.
int draw_sw
1 if SW edge (S->W, bottom-left) should be drawn
int neighbor_top_sw
top height of SW neighbor (0 if absent/boundary)
float angle_deg
current projection angle in degrees
int smooth_height
0 = CUT mode (cliff walls), 1 = SMOOTH mode (per-corner slopes)
int * neighbor_terrains_north
[width], terrain of north neighbor's south edge
ISO_DRAW_ENTRY * cached_draw_order
Cached draw_order.
float half_w
half-width of a tile in pixels (horizontal extent)
int count
0..ISO_MAX_SEGMENTS_PER_TILE
int bottom
lower height bound (inclusive), must be < top
int hover_mx
hovered tile X (-1 = none)
int max_height
maximum allowed height value
int top
segment top height
int seg_idx
segment index within tile (0..count-1)
int draw_nw
1 if NW edge (N->W, top-left) should be drawn
int underside
1 if underside face should be drawn
int neighbor_top_ne
top height of NE neighbor (0 if absent/boundary)
float occlude_clip_y
OUTPUT: screen Y of top occluder north vertex (for cross-chunk clip fallback).
int num_terrains
number of terrain types used
float * dynamic_light_map
Per-tile dynamic light accumulation buffer.
int top
upper height bound, must be > bottom
int * neighbor_terrains_east
[height], terrain of east neighbor's west edge
float half_h
half-height of a tile in pixels (vertical extent)
int * overlay
overlay tile index per cell [height * width], 0 = none.
float lerp_speed
interpolation speed factor (default 3.0)
int neighbor_top_nw
top height of NW neighbor (0 if absent/boundary)
int * neighbor_terrains_west
[height], terrain of west neighbor's east edge
int * neighbor_terrains_south
[width], terrain of south neighbor's north edge
int * neighbor_heights_south
Heights from south neighbor chunk's north edge [width], NULL = boundary.
int * terrain
terrain type per cell [height * width], indices 0..num_terrains-1
N_ISO_OBJECT_DRAW_FN draw
draw callback
float zoom_max
maximum allowed zoom
int * neighbor_heights_north
Heights from north neighbor chunk's south edge [width], NULL = boundary.
float height_tint_intensity
0.0 = no tint, 0.3 = subtle, 1.0 = full range.
float zoom
zoom factor (1.0 = no zoom)
float ambient_b
int is_occluded
OUTPUT: set to 1 if behind tiles during last iso_map_draw() call.
int draw_se
1 if SE edge (E->S, bottom-right) should be drawn
int my
tile coordinates
int cached_draw_order_count
int * heightmap
height value per cell [height * width], 0..max_height
float y
camera Y offset (world units, pre-zoom)
float ambient_r
Global ambient color (day/night + dark place tinting).
int width
map width in tiles (X axis)
float ambient_g
int show_grid
1 = draw grid overlay
float target_angle
target angle for smooth interpolation (degrees)
int draw_ne
1 if NE edge (N->E, top-right) should be drawn
ISO_TILE_SEGMENTS * segments
Per-cell height segments.
int draw_underside
1 if floating segment underside border should be drawn
int smooth_slope_max
max height diff rendered as slope (default 1)
int hover_my
hovered tile Y (-1 = none)
int cached_draw_order_cap
float x
camera X offset (world units, pre-zoom)
ISO_PROJECTION proj
current projection parameters
double n_iso_pass1_us
Per-pass timing accumulators populated by iso_map_draw.
void iso_map_set_height(ISO_MAP *map, int mx, int my, int h)
Set the height at a cell (clamped to [0, max_height]).
void iso_map_set_projection_custom(ISO_MAP *map, float half_w, float half_h, float tile_lift)
Set custom projection parameters directly.
#define ISO_EDGE_S
Edge bitmask: south neighbor has higher terrain.
ALLEGRO_COLOR(* IsoPrimitiveColorFilter)(ALLEGRO_COLOR c)
Optional design-RGB to display-RGB filter applied to every primitive color (filled polygons,...
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)
Compute smooth corner heights with slope clamping.
#define SWIM
FLAG of a swimmable tile.
double n_iso_setup_us
ISO_MAP * iso_map_new(int width, int height, int num_terrains, int max_height)
Create a new height-aware isometric map.
float iso_diamond_dist(int px, int py, int tile_w, int tile_h)
Distance from pixel to the diamond edge.
double n_iso_pass2_us
int iso_map_set_segments(ISO_MAP *map, int mx, int my, const ISO_TILE_SEGMENT *segs, int count)
set per-tile segments on an ISO_MAP.
void n_iso_camera_zoom(N_ISO_CAMERA *cam, float dz, float mouse_x, float mouse_y)
Zoom the camera toward a screen-space point.
void n_iso_camera_center_on(N_ISO_CAMERA *cam, float world_x, float world_y, int screen_w, int screen_h)
Center the camera so that a world point is at screen center.
double n_iso_pass3_us
#define ISO_NUM_EDGE_MASKS
Number of possible edge mask combinations (2^4 = 16).
int iso_map_save(const ISO_MAP *map, const char *filename)
Save ISO_MAP to a binary file.
ALLEGRO_COLOR iso_apply_primitive_color_filter(ALLEGRO_COLOR c)
int iso_map_should_transition_smooth(const ISO_MAP *map, int mx1, int my1, int mx2, int my2)
Check if terrain transition should render between two cells (height-aware).
#define ISO_PROJ_ISOMETRIC
Projection ID: true isometric (30 degree angle).
#define WALK
FLAG of a walkable tile.
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)
Project a tile corner to screen coordinates (canonical formula).
#define ISO_PROJ_STAGGERED
Projection ID: flatter dimetric (~18.43 degree angle).
void iso_set_line_jitter_compensation(IsoLineJitterCompensationFn fn)
void iso_map_lerp_projection(ISO_MAP *map, float dt)
Smoothly interpolate the projection angle toward the target.
#define ISO_MAX_SEGMENTS_PER_TILE
Max height segments per tile (ground + one elevated structure).
void iso_mask_tile_to_diamond(ALLEGRO_BITMAP *bmp, int tile_w, int tile_h)
Mask a tile bitmap to the isometric diamond shape.
ISO_VISIBLE_EDGES iso_map_get_visible_edges(const ISO_MAP *map, int mx, int my)
Compute which diamond edges of tile (mx,my) need a height border.
#define ISO_NUM_CORNER_MASKS
Number of possible corner mask combinations (2^4 = 16).
ISO_MAP * iso_map_load(const char *filename)
Load ISO_MAP from a binary file.
void n_iso_camera_free(N_ISO_CAMERA **cam)
Free a camera and set the pointer to NULL.
#define ISO_EDGE_N
Edge bitmask: north neighbor has higher terrain.
int iso_map_build_draw_order(const ISO_MAP *map, ISO_DRAW_ENTRY *out, int max_entries)
Build the draw order for segment-sorted rendering.
int iso_map_should_transition(const ISO_MAP *map, int mx1, int my1, int mx2, int my2)
Check if terrain blending should occur between two adjacent cells.
const char * iso_projection_name(int preset)
Get the display name for a projection preset.
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)
Smoothly follow a world-space target.
void iso_map_set_ability(ISO_MAP *map, int mx, int my, int ab)
Set the ability at a cell.
ISO_VISIBLE_EDGES iso_map_get_visible_edges_segment(const ISO_MAP *map, int mx, int my, int seg_idx)
Per-segment edge visibility with adjacency + occlusion.
int iso_map_get_terrain(const ISO_MAP *map, int mx, int my)
Get the terrain type at a cell.
void iso_map_calc_transitions_full(const ISO_MAP *map, int mx, int my, int *edge_bits, int *corner_bits)
Compute per-terrain transition bitmasks with height filtering.
int iso_map_get_ability(const ISO_MAP *map, int mx, int my)
Get the ability at a cell.
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)
Draw the full ISO_MAP with segment-sorted rendering.
void iso_map_to_screen(const ISO_MAP *map, int mx, int my, int h, float *screen_x, float *screen_y)
Convert map tile coordinates to screen pixel coordinates.
void iso_map_calc_transitions(const ISO_MAP *map, int mx, int my, int *edge_bits, int *corner_bits)
Compute terrain transition bitmasks for a cell (Article 934).
void iso_map_set_projection(ISO_MAP *map, int preset, float tile_width)
Set projection parameters from a preset and tile width.
#define ISO_EDGE_E
Edge bitmask: east neighbor has higher terrain.
void n_iso_camera_screen_to_world(const N_ISO_CAMERA *cam, float sx, float sy, float *wx, float *wy)
Convert screen pixel coordinates to world coordinates.
void iso_map_to_screen_f(const ISO_MAP *map, float fmx, float fmy, float h, float *screen_x, float *screen_y)
Convert map coordinates to screen coordinates (float version).
void iso_map_free(ISO_MAP **map_ptr)
Free an ISO_MAP and set the pointer to NULL.
int iso_is_in_diamond(int px, int py, int tile_w, int tile_h)
Test if pixel (px,py) is inside the isometric diamond.
void iso_generate_transition_tiles(ALLEGRO_BITMAP ***tiles, ALLEGRO_BITMAP **masks, ALLEGRO_BITMAP **tile_bitmaps, int num_terrains, int tile_w, int tile_h)
Pre-composite transition tiles (terrain texture * alpha mask).
#define ISO_CORNER_SE
Corner bitmask: southeast diagonal has higher terrain.
#define ISO_NUM_MASKS
Total number of transition masks (edge + corner).
#define ISO_CORNER_NE
Corner bitmask: northeast diagonal has higher terrain.
void iso_map_set_projection_target(ISO_MAP *map, int preset)
Set the target projection for smooth interpolation.
#define ISO_CORNER_SW
Corner bitmask: southwest diagonal has higher terrain.
void iso_set_primitive_color_filter(IsoPrimitiveColorFilter fn)
void iso_map_set_terrain(ISO_MAP *map, int mx, int my, int terrain)
Set the terrain type at a cell.
void iso_screen_to_map(const ISO_MAP *map, float screen_x, float screen_y, int *mx, int *my)
Convert screen pixel coordinates to map tile coordinates.
void iso_map_randomize(ISO_MAP *map)
Randomize terrain and height for testing/demo purposes.
void iso_generate_transition_masks(ALLEGRO_BITMAP **masks, int tile_w, int tile_h)
Generate the 32 procedural transition alpha masks (16 edge + 16 corner).
#define ISO_CORNER_NW
Corner bitmask: northwest diagonal has higher terrain.
void(* IsoLineJitterCompensationFn)(float *out_dx, float *out_dy)
const ISO_TILE_SEGMENTS * iso_map_get_segments(const ISO_MAP *map, int mx, int my)
get per-tile segments.
int iso_map_get_height(const ISO_MAP *map, int mx, int my)
Get the height at a cell.
float iso_map_interpolate_height(const ISO_MAP *map, float fx, float fy)
Bilinear height interpolation at fractional map coordinates.
#define ISO_PROJ_CLASSIC
Projection ID: classic 2:1 isometric (~26.565 degree angle).
void n_iso_camera_world_to_screen(const N_ISO_CAMERA *cam, float wx, float wy, float *sx, float *sy)
Convert world coordinates to screen pixel coordinates.
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)
Height-aware screen-to-map conversion returning fractional tile coordinates.
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)
Height-aware screen-to-map conversion with diamond hit testing.
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)
Compute average corner heights for smooth tile rendering (Article 2026).
#define ISO_EDGE_W
Edge bitmask: west neighbor has higher terrain.
void iso_apply_line_jitter_compensation(float *out_dx, float *out_dy)
void n_iso_camera_scroll(N_ISO_CAMERA *cam, float dx, float dy)
Scroll the camera by (dx, dy) world units.
N_ISO_CAMERA * n_iso_camera_new(float zoom_min, float zoom_max)
Create a new 2D isometric camera.
#define ISO_PROJ_MILITARY
Projection ID: military/planometric (45 degree angle).
Draw order entry for segment-sorted rendering.
Height-aware isometric map with terrain and height layers, per-cell height values,...
Single vertical segment of a tile: solid matter from bottom to top.
Per-tile height segments.
Per-tile edge visibility flags for height border rendering.
2D isometric camera for viewport management.
Drawable object for depth-sorted isometric rendering.
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
#define LOG_INFO
informational
Definition n_log.h:106
A* Pathfinding API for 2D and 3D grids.
static FILE * _iso_fopen_write(const char *path, const char *mode)
static int _neighbor_terrain_at(const ISO_MAP *map, int mx, int my)
Edge-aware terrain lookup for chunked rendering.
static float _smooth_neighbor_h(const ISO_MAP *map, float h_self, int nx, int ny)
Helper: clamp neighbor height for smooth slope rendering.
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}.
static float _iso_preset_angle(int preset)
Get the angle in degrees for a projection preset.
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.
static int _neighbor_height_at(const ISO_MAP *map, int mx, int my)
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.
static 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)
static int _neighbor_should_transition(const ISO_MAP *map, int mx1, int my1, int mx2, int my2)
static IsoLineJitterCompensationFn g_iso_line_jitter_fn
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.
static 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).
static IsoPrimitiveColorFilter g_iso_primitive_color_filter
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.
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.
static int _cmp_draw_entry(const void *a, const void *b)
Isometric/axonometric tile engine with height maps, terrain transitions, and A* pathfinding integrati...