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
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
1594static inline ALLEGRO_COLOR _height_tint(int seg_top, int max_height, float intensity) {
1595 if (intensity <= 0.0f || max_height <= 0)
1596 return al_map_rgba_f(1.0f, 1.0f, 1.0f, 1.0f);
1597 float ratio = (float)seg_top / (float)max_height;
1598 /* brightness ranges from (1 - intensity) at height 0 to 1.0 at max_height */
1599 float b = 1.0f - intensity * (1.0f - ratio);
1600 if (b < 0.0f) b = 0.0f;
1601 if (b > 1.0f) b = 1.0f;
1602 return al_map_rgba_f(b, b, b, 1.0f);
1603}
1604
1605/* Extended tint: combines height tint with ambient color and per-tile dynamic light.
1606 * ambient_r/g/b: global day/night ambient (1.0 = full daylight).
1607 * dyn_r/g/b: additive dynamic light contribution at this tile (0.0 = no light). */
1608static 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) {
1609 /* Start with height-based brightness */
1610 float hb = 1.0f;
1611 if (intensity > 0.0f && max_height > 0) {
1612 float ratio = (float)seg_top / (float)max_height;
1613 hb = 1.0f - intensity * (1.0f - ratio);
1614 if (hb < 0.0f) hb = 0.0f;
1615 if (hb > 1.0f) hb = 1.0f;
1616 }
1617
1618 /* Multiply by ambient color */
1619 float r = hb * ambient_r;
1620 float g = hb * ambient_g;
1621 float b = hb * ambient_b;
1622
1623 /* Add dynamic light (additive boost) */
1624 r += dyn_r;
1625 g += dyn_g;
1626 b += dyn_b;
1627
1628 /* Clamp to avoid overexposure */
1629 if (r > 1.0f) r = 1.0f;
1630 if (g > 1.0f) g = 1.0f;
1631 if (b > 1.0f) b = 1.0f;
1632
1633 /* The tint multiplier is sRGB-designed (ambient curve + dyn-light
1634 * literals tuned on a non-sRGB display). Route through the primitive
1635 * color filter so an sRGB-aware consumer can linearise it before it
1636 * multiplies the linear-decoded texel inside an sRGB FBO. Default
1637 * filter = identity, so the non-sRGB path is byte-identical. */
1638 return iso_apply_primitive_color_filter(al_map_rgba_f(r, g, b, 1.0f));
1639}
1640
1647static void _draw_tile_warped(ALLEGRO_BITMAP* bmp,
1648 const float vx[4],
1649 const float vy[4],
1650 int tile_w,
1651 int tile_h,
1652 ALLEGRO_COLOR tint) {
1653 /* al_draw_prim samples u/v against the PARENT texture and does NOT
1654 * auto-translate for a sub-bitmap (unlike al_draw_*_bitmap). When tiles
1655 * are packed into a terrain atlas (each tile an al_create_sub_bitmap),
1656 * passing sub-local UVs [0..tile_w] samples the atlas ORIGIN, which is
1657 * transparent padding, so warped (sloped) tiles render BLACK. Resolve
1658 * the parent + add the sub-bitmap's atlas offset to every UV and draw
1659 * against the parent so the warp samples the tile's real atlas region.
1660 * For a standalone (non-sub) bitmap the offsets are 0 and parent == bmp,
1661 * so this is byte-identical to the pre-atlas behaviour. */
1662 ALLEGRO_BITMAP* tex = al_get_parent_bitmap(bmp);
1663 float ox, oy;
1664 if (tex) {
1665 ox = (float)al_get_bitmap_x(bmp);
1666 oy = (float)al_get_bitmap_y(bmp);
1667 } else {
1668 tex = bmp;
1669 ox = 0.0f;
1670 oy = 0.0f;
1671 }
1672 float un = ox + (float)tile_w / 2.0f, vn = oy + 0.0f;
1673 float ue = ox + (float)tile_w, ve = oy + (float)tile_h / 2.0f;
1674 float us = ox + (float)tile_w / 2.0f, vs = oy + (float)tile_h;
1675 float uw = ox + 0.0f, vw = oy + (float)tile_h / 2.0f;
1676 ALLEGRO_VERTEX vtx[6];
1677 vtx[0].x = vx[0];
1678 vtx[0].y = vy[0];
1679 vtx[0].z = 0;
1680 vtx[0].u = un;
1681 vtx[0].v = vn;
1682 vtx[0].color = tint;
1683 vtx[1].x = vx[1];
1684 vtx[1].y = vy[1];
1685 vtx[1].z = 0;
1686 vtx[1].u = ue;
1687 vtx[1].v = ve;
1688 vtx[1].color = tint;
1689 vtx[2].x = vx[2];
1690 vtx[2].y = vy[2];
1691 vtx[2].z = 0;
1692 vtx[2].u = us;
1693 vtx[2].v = vs;
1694 vtx[2].color = tint;
1695 vtx[3].x = vx[0];
1696 vtx[3].y = vy[0];
1697 vtx[3].z = 0;
1698 vtx[3].u = un;
1699 vtx[3].v = vn;
1700 vtx[3].color = tint;
1701 vtx[4].x = vx[2];
1702 vtx[4].y = vy[2];
1703 vtx[4].z = 0;
1704 vtx[4].u = us;
1705 vtx[4].v = vs;
1706 vtx[4].color = tint;
1707 vtx[5].x = vx[3];
1708 vtx[5].y = vy[3];
1709 vtx[5].z = 0;
1710 vtx[5].u = uw;
1711 vtx[5].v = vw;
1712 vtx[5].color = tint;
1713
1714 al_draw_prim(vtx, NULL, tex, 0, 6, ALLEGRO_PRIM_TRIANGLE_LIST);
1715}
1716
1722static void _draw_segment_sides(const ISO_MAP* map,
1723 float east_x,
1724 float east_y,
1725 float south_x,
1726 float south_y,
1727 float west_x,
1728 float west_y,
1729 int mx,
1730 int my,
1731 int seg_bottom,
1732 int seg_top,
1733 float lift,
1734 float height_brightness,
1735 int wall_terrain_override) {
1736 int base = (wall_terrain_override >= 0) ? wall_terrain_override
1737 : iso_map_get_terrain(map, mx, my);
1738 float east_shade = 0.55f;
1739 float south_shade = 0.35f;
1740
1741 float cr, cg, cb;
1742 switch (base) {
1743 case 0:
1744 cr = 0.7f;
1745 cg = 0.25f;
1746 cb = 0.1f;
1747 break;
1748 case 1:
1749 cr = 0.5f;
1750 cg = 0.45f;
1751 cb = 0.35f;
1752 break;
1753 case 2:
1754 cr = 0.6f;
1755 cg = 0.55f;
1756 cb = 0.45f;
1757 break;
1758 default:
1759 cr = 0.4f;
1760 cg = 0.35f;
1761 cb = 0.3f;
1762 break;
1763 }
1764 ALLEGRO_COLOR east_col = iso_apply_primitive_color_filter(
1765 al_map_rgba_f(cr * east_shade * height_brightness,
1766 cg * east_shade * height_brightness,
1767 cb * east_shade * height_brightness, 1.0f));
1768 ALLEGRO_COLOR south_col = iso_apply_primitive_color_filter(
1769 al_map_rgba_f(cr * south_shade * height_brightness,
1770 cg * south_shade * height_brightness,
1771 cb * south_shade * height_brightness, 1.0f));
1772 /* Keep PASS 1 on ONE blender throughout to avoid held-batch
1773 * cross-tile contamination. Cliff wall fills use alpha=1.0 ->
1774 * premul == straight, and edge lines are RGB=0 -> premul ==
1775 * straight, so this switch is a visual no-op. */
1776 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
1777
1778 int east_face_h, south_face_h;
1779 if (seg_bottom == 0) {
1780 /* Ground segment: use neighbor-aware wall height (existing logic) */
1781 int h_e = iso_map_get_height(map, mx + 1, my);
1782 int h_s = iso_map_get_height(map, mx, my + 1);
1783 int h_se = iso_map_get_height(map, mx + 1, my + 1);
1784
1785 east_face_h = seg_top - (h_e < h_se ? h_e : h_se);
1786 // cppcheck-suppress redundantAssignment ; fallback chain: min(h_e,h_se) -> h_e -> h_se
1787 if (east_face_h < 1) east_face_h = seg_top - h_e;
1788 if (east_face_h < 1) east_face_h = seg_top - h_se;
1789
1790 south_face_h = seg_top - (h_s < h_se ? h_s : h_se);
1791 // cppcheck-suppress redundantAssignment ; fallback chain: min(h_s,h_se) -> h_s -> h_se
1792 if (south_face_h < 1) south_face_h = seg_top - h_s;
1793 if (south_face_h < 1) south_face_h = seg_top - h_se;
1794 } else {
1795 /* Floating segment: full wall from bottom to top */
1796 east_face_h = seg_top - seg_bottom;
1797 south_face_h = seg_top - seg_bottom;
1798 }
1799
1800 /* No sub-pixel expansion needed: all vertices come from the canonical
1801 * _iso_corner_to_screen formula, so adjacent tiles sharing an edge
1802 * produce bit-identical vertex positions. */
1803
1804 /* Query the client-registered jitter source. Returns (0, 0) when
1805 * no jitter source is active. When non-zero, we subtract it from
1806 * every line endpoint so the line lands at the same screen pixel
1807 * each frame regardless of where in the TAA jitter cycle we are.
1808 * Filled polygon faces deliberately KEEP the jitter: they are
1809 * the larger draws TAA's history blend smooths perceptually. */
1810 float jx = 0.0f, jy = 0.0f;
1812
1813 if (east_face_h > 0) {
1814 float drop = (float)east_face_h * lift;
1815 float v[8] = {east_x, east_y, south_x, south_y,
1816 south_x, south_y + drop,
1817 east_x, east_y + drop};
1818 al_draw_filled_polygon(v, 4, east_col);
1819 ALLEGRO_COLOR edge = al_map_rgba(0, 0, 0, 80);
1820 al_draw_line(east_x - jx, east_y + drop - jy,
1821 south_x - jx, south_y + drop - jy,
1822 edge, 1.0f);
1823 }
1824
1825 if (south_face_h > 0) {
1826 float drop = (float)south_face_h * lift;
1827 float v[8] = {south_x, south_y, west_x, west_y,
1828 west_x, west_y + drop,
1829 south_x, south_y + drop};
1830 al_draw_filled_polygon(v, 4, south_col);
1831 ALLEGRO_COLOR edge = al_map_rgba(0, 0, 0, 80);
1832 al_draw_line(west_x - jx, west_y + drop - jy,
1833 south_x - jx, south_y + drop - jy,
1834 edge, 1.0f);
1835 }
1836}
1837
1842static void _draw_segment_underside(ALLEGRO_BITMAP* bmp,
1843 float base_dx,
1844 float base_dy,
1845 float phw,
1846 float phh,
1847 float tile_draw_w,
1848 float tile_draw_h,
1849 int tile_w,
1850 int tile_h) {
1851 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
1852 /* Draw the tile bitmap with a darkened tint (65% brightness) */
1853 ALLEGRO_COLOR tint = al_map_rgba_f(0.65f, 0.65f, 0.65f, 1.0f);
1854 (void)phw;
1855 (void)phh;
1856 al_draw_tinted_scaled_bitmap(bmp, tint,
1857 0, 0, (float)tile_w, (float)tile_h,
1858 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
1859}
1860
1882 float obj_fx,
1883 float obj_fy,
1884 float obj_fz,
1885 float obj_sx,
1886 float obj_sy,
1887 float sprite_h,
1888 float sprite_half_w,
1889 float cam_px,
1890 float cam_py,
1891 float zoom,
1892 float* out_clip_y) {
1893 /* Hoist projection constants out of the inner loop. */
1894 const float proj_hw = map->proj.half_w;
1895 const float proj_hh = map->proj.half_h;
1896 const float proj_lift = map->proj.tile_lift;
1897
1898 float phw = proj_hw * zoom;
1899 float phh = proj_hh * zoom;
1900 float lift = proj_lift * zoom;
1901
1902 /* Entity head screen Y: sprite_h world units above feet */
1903 float head_sy = obj_sy - sprite_h * lift;
1904 /* Entity horizontal extent (screen pixels) */
1905 float ent_left = obj_sx - sprite_half_w * zoom;
1906 float ent_right = obj_sx + sprite_half_w * zoom;
1907
1908 int base_mx = (int)floorf(obj_fx);
1909 int base_my = (int)floorf(obj_fy);
1910 int check_range = map->max_height + 2;
1911 int found = 0;
1912 float best_clip_y = 1e9f;
1913
1914 for (int dy = -1; dy <= check_range; dy++) {
1915 for (int dx = -1; dx <= check_range; dx++) {
1916 /* Only check tiles drawn after the object in painter's order.
1917 * Filter by isometric depth (dx+dy) not just grid position. */
1918 int tdepth = dx + dy;
1919 if (tdepth < 0) continue;
1920 if (tdepth == 0 && dy < 0) continue;
1921 if (tdepth == 0 && dy == 0 && dx <= 0) continue;
1922
1923 int tmx = base_mx + dx;
1924 int tmy = base_my + dy;
1925 if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height) continue;
1926
1927 int th = iso_map_get_height(map, tmx, tmy);
1928 /* tile must reach above the entity's feet to potentially occlude */
1929 if ((float)th <= obj_fz) continue;
1930
1931 /* inlined iso_map_to_screen(map, tmx, tmy, th, &tsx, &tsy) */
1932 float tsx = ((float)tmx - (float)tmy) * proj_hw + proj_hw;
1933 float tsy = ((float)tmx + (float)tmy) * proj_hh - (float)th * proj_lift;
1934 float tile_dx = tsx * zoom + cam_px;
1935 float tile_dy = tsy * zoom + cam_py;
1936
1937 /* tile screen bounding box */
1938 float tile_left = tile_dx;
1939 float tile_right = tile_dx + 2.0f * phw;
1940 float tile_top = tile_dy;
1941 float tile_bot = tile_dy + 2.0f * phh + (float)th * lift;
1942
1943 /* Check horizontal overlap (entity range vs tile range) */
1944 if (ent_right < tile_left || ent_left > tile_right) continue;
1945
1946 /* Check vertical overlap (any part of entity behind tile) */
1947 if (tile_top <= obj_sy && tile_bot >= head_sy) {
1948 found = 1;
1949 /* Track north vertex of occluding tile for clip-Y */
1950 float nx, ny;
1951 _iso_corner_to_screen(proj_hw, proj_hh, proj_lift,
1952 tmx, tmy, (float)th,
1953 cam_px, cam_py, zoom, &nx, &ny);
1954 if (ny < best_clip_y) best_clip_y = ny;
1955 }
1956 }
1957 }
1958 if (out_clip_y) *out_clip_y = best_clip_y;
1959 return found;
1960}
1961
1975static void _draw_height_borders(const ISO_MAP* map,
1976 int mx,
1977 int my,
1978 int seg_idx,
1979 int seg_bottom,
1980 int seg_top,
1981 int has_underside_face,
1982 const float vx[4],
1983 const float vy[4],
1984 float lift) {
1985 ISO_VISIBLE_EDGES edges = iso_map_get_visible_edges_segment(map, mx, my, seg_idx);
1986 if (!edges.draw_nw && !edges.draw_ne && !edges.draw_se && !edges.draw_sw && !(has_underside_face && edges.draw_underside))
1987 return;
1988
1989 /* Subtract the client-registered jitter (e.g. the consumer's TAA
1990 * sub-pixel offset) from every line endpoint so these decorative
1991 * borders stay pixel-stable across frames while sprites and
1992 * filled faces continue to inherit the jitter. Returns (0, 0)
1993 * when no jitter source is active. */
1994 float jx = 0.0f, jy = 0.0f;
1996
1997 ALLEGRO_COLOR border_col = al_map_rgba(0, 0, 0, 200);
1998 float thickness = 1.5f;
1999
2000 /* Top face edges */
2001 if (edges.draw_se)
2002 al_draw_line(vx[1] - jx, vy[1] - jy,
2003 vx[2] - jx, vy[2] - jy, border_col, thickness);
2004 if (edges.draw_sw)
2005 al_draw_line(vx[2] - jx, vy[2] - jy,
2006 vx[3] - jx, vy[3] - jy, border_col, thickness);
2007 if (edges.draw_nw)
2008 al_draw_line(vx[0] - jx, vy[0] - jy,
2009 vx[3] - jx, vy[3] - jy, border_col, thickness);
2010 if (edges.draw_ne)
2011 al_draw_line(vx[0] - jx, vy[0] - jy,
2012 vx[1] - jx, vy[1] - jy, border_col, thickness);
2013
2014 /* Vertical corner lines.
2015 *
2016 * For each drawn edge where seg_top > neighbor_top (A is higher):
2017 * Draw a vertical line at BOTH endpoints of that edge,
2018 * from tileToScreen(vertex, seg_top) down to
2019 * tileToScreen(vertex, neighbor_top).
2020 * Exception: NEVER draw at the North vertex (vx[0]/vy[0]).
2021 *
2022 * If seg_top <= neighbor_top: no vertical lines (wall belongs to N).
2023 *
2024 * vx[]/vy[] are already at tileToScreen(vertex, seg_top).
2025 * The bottom endpoint is (seg_top - HN) * lift pixels below.
2026 *
2027 * Diamond vertices: N=0, E=1, S=2, W=3.
2028 * Edge endpoints: NE=(N,E) SE=(E,S) SW=(S,W) NW=(W,N)
2029 *
2030 * Multiple edges may want to draw at the same vertex with different
2031 * drops. We draw the longest line (smallest HN) at each vertex
2032 * to avoid duplicate shorter lines.
2033 */
2034 {
2035 /* Per-vertex: track the largest drop (longest line) requested.
2036 *
2037 * CONTINUITY CHECK: a vertical corner line at a vertex is only
2038 * drawn if the wall face ENDS at that vertex, i.e. the adjacent
2039 * tile in the wall's direction is NOT at the same height.
2040 * If the adjacent tile IS at the same height, the wall surface
2041 * continues past the vertex and the line is interior (hidden).
2042 *
2043 * For the east-facing wall (SE edge):
2044 * E vertex: wall continues northward if NE neighbor same height -> skip
2045 * S vertex: wall continues southward if SW neighbor same height -> skip
2046 * For the south-facing wall (SW edge):
2047 * S vertex: wall continues eastward if SE neighbor same height -> skip
2048 * W vertex: wall continues westward if NW neighbor same height -> skip
2049 * For NE edge -> E vertex: wall continues if SE neighbor same height
2050 * For NW edge -> W vertex: wall continues if SW neighbor same height
2051 */
2052 float drop_e = 0, drop_s = 0, drop_w = 0;
2053 int nt_nw = edges.neighbor_top_nw;
2054 int nt_ne = edges.neighbor_top_ne;
2055 int nt_se = edges.neighbor_top_se;
2056 int nt_sw = edges.neighbor_top_sw;
2057
2058 /* Clamp neighbor tops to seg_bottom: vertical lines must not
2059 * extend below the segment's own base (floating segments). */
2060 int bot = seg_bottom;
2061 int cl_nw = nt_nw > bot ? nt_nw : bot;
2062 int cl_ne = nt_ne > bot ? nt_ne : bot;
2063 int cl_se = nt_se > bot ? nt_se : bot;
2064 int cl_sw = nt_sw > bot ? nt_sw : bot;
2065
2066 /* NE edge (N,E): only E vertex (N is never drawn) */
2067 if (edges.draw_ne && seg_top > cl_ne) {
2068 /* E vertex: east wall continues if SE neighbor same height */
2069 if (nt_se != seg_top) {
2070 float d = (float)(seg_top - cl_ne) * lift;
2071 if (d > drop_e) drop_e = d;
2072 }
2073 }
2074 /* SE edge (E,S): E and S vertices */
2075 if (edges.draw_se && seg_top > cl_se) {
2076 float d = (float)(seg_top - cl_se) * lift;
2077 /* E vertex: east wall continues northward if NE neighbor same height */
2078 if (nt_ne != seg_top) {
2079 if (d > drop_e) drop_e = d;
2080 }
2081 /* S vertex: east wall continues southward if SW neighbor same height */
2082 if (nt_sw != seg_top) {
2083 if (d > drop_s) drop_s = d;
2084 }
2085 }
2086 /* SW edge (S,W): S and W vertices */
2087 if (edges.draw_sw && seg_top > cl_sw) {
2088 float d = (float)(seg_top - cl_sw) * lift;
2089 /* S vertex: south wall continues eastward if SE neighbor same height */
2090 if (nt_se != seg_top) {
2091 if (d > drop_s) drop_s = d;
2092 }
2093 /* W vertex: south wall continues westward if NW neighbor same height */
2094 if (nt_nw != seg_top) {
2095 if (d > drop_w) drop_w = d;
2096 }
2097 }
2098 /* NW edge (W,N): only W vertex (N is never drawn) */
2099 if (edges.draw_nw && seg_top > cl_nw) {
2100 /* W vertex: south wall continues if SW neighbor same height */
2101 if (nt_sw != seg_top) {
2102 float d = (float)(seg_top - cl_nw) * lift;
2103 if (d > drop_w) drop_w = d;
2104 }
2105 }
2106
2107 if (drop_e > 0)
2108 al_draw_line(vx[1] - jx, vy[1] - jy,
2109 vx[1] - jx, vy[1] + drop_e - jy,
2110 border_col, thickness);
2111 if (drop_s > 0)
2112 al_draw_line(vx[2] - jx, vy[2] - jy,
2113 vx[2] - jx, vy[2] + drop_s - jy,
2114 border_col, thickness);
2115 if (drop_w > 0)
2116 al_draw_line(vx[3] - jx, vy[3] - jy,
2117 vx[3] - jx, vy[3] + drop_w - jy,
2118 border_col, thickness);
2119 }
2120
2121 /* Underside face border (floating segments only).
2122 * The underside diamond is at seg_bottom, offset below the top face
2123 * by (seg_top - seg_bottom) * lift pixels. Only drawn if the
2124 * underside face itself was rendered (has_underside_face).
2125 * Only SE and SW edges, NW and NE are always hidden behind the
2126 * segment's own side walls from the camera angle. */
2127 if (has_underside_face && edges.draw_underside) {
2128 float dy = (float)(seg_top - seg_bottom) * lift;
2129 /* SE edge */
2130 al_draw_line(vx[1] - jx, vy[1] + dy - jy,
2131 vx[2] - jx, vy[2] + dy - jy,
2132 border_col, thickness);
2133 /* SW edge */
2134 al_draw_line(vx[2] - jx, vy[2] + dy - jy,
2135 vx[3] - jx, vy[3] + dy - jy,
2136 border_col, thickness);
2137 }
2138}
2139
2159void iso_map_draw(const ISO_MAP* map,
2160 ALLEGRO_BITMAP** tile_bitmaps,
2161 ALLEGRO_BITMAP*** transition_tiles,
2162 int num_masks,
2163 ALLEGRO_BITMAP** overlay_bitmaps,
2164 int num_overlay_tiles,
2165 float cam_px,
2166 float cam_py,
2167 float zoom,
2168 int screen_w,
2169 int screen_h,
2170 int player_mode,
2171 N_ISO_OBJECT* objects,
2172 int num_objects) {
2173 __n_assert(map, return);
2174 __n_assert(tile_bitmaps, return);
2175
2176 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2177
2178 float hw0 = map->proj.half_w;
2179 float hh0 = map->proj.half_h;
2180 float tl0 = map->proj.tile_lift;
2181 float phw = hw0 * zoom;
2182 float phh = hh0 * zoom;
2183 float lift = tl0 * zoom;
2184 float tile_draw_w = 2.0f * phw;
2185 float tile_draw_h = 2.0f * phh;
2186
2187 /* cam_px / cam_py are pre-computed pixel offsets provided by the caller.
2188 * The caller may pass either integer-snapped (`floorf(cam_x * zoom)`) or
2189 * sub-pixel (`cam_x * zoom`) values. Sub-pixel is preferred when paired
2190 * with MIN_LINEAR bilinear sampling for smooth camera motion. Integer
2191 * snap pairs with NEAREST sampling for crisp pixel-art but produces
2192 * "world jumps by 1 pixel" stutter during motion. Chunk pixel offsets
2193 * are added on top of whatever value the caller passes: they remain
2194 * integer at zoom = 1 regardless. */
2195
2196 int tile_w = tile_bitmaps[0] ? al_get_bitmap_width(tile_bitmaps[0]) : 64;
2197 int tile_h = tile_bitmaps[0] ? al_get_bitmap_height(tile_bitmaps[0]) : 32;
2198
2199 int num_edge_masks = ISO_NUM_EDGE_MASKS;
2200
2201 double _iso_t_setup = al_get_time();
2202
2203 /* Build segment draw order.
2204 *
2205 * The draw_order is cached on the ISO_MAP and only rebuilt when
2206 * the segment/height layout actually changes (`draw_order_dirty`
2207 * is set by iso_map_set_segments). For a static-terrain scene
2208 * this drops to a zero-work hit per frame. obj_order / obj_sx_arr
2209 * / obj_sy_arr stay file-static because objects' world positions
2210 * change every frame and the buffer just needs to be big enough;
2211 * per-map ownership isn't required. */
2212 int max_draw_entries = map->width * map->height * ISO_MAX_SEGMENTS_PER_TILE;
2213 ISO_MAP* mut_map = (ISO_MAP*)map; /* the cache fields are deliberately writable */
2214 if (mut_map->cached_draw_order_cap < max_draw_entries) {
2215 ISO_DRAW_ENTRY* grown = (ISO_DRAW_ENTRY*)realloc(
2216 mut_map->cached_draw_order,
2217 (size_t)max_draw_entries * sizeof(ISO_DRAW_ENTRY));
2218 if (!grown) return;
2219 mut_map->cached_draw_order = grown;
2220 mut_map->cached_draw_order_cap = max_draw_entries;
2221 mut_map->draw_order_dirty = 1;
2222 }
2223 if (mut_map->draw_order_dirty) {
2225 map, mut_map->cached_draw_order, mut_map->cached_draw_order_cap);
2226 mut_map->draw_order_dirty = 0;
2227 }
2228 ISO_DRAW_ENTRY* draw_order = mut_map->cached_draw_order;
2229 int num_entries = mut_map->cached_draw_order_count;
2230
2231 /* Pre-process objects: compute screen positions and sort by depth.
2232 * Sort key matches segment draw order: (floor(fz), my, mx). */
2233 int nobj = (objects && num_objects > 0) ? num_objects : 0;
2234 static int* obj_order = NULL;
2235 static float* obj_sx_arr = NULL;
2236 static float* obj_sy_arr = NULL;
2237 static int obj_buf_cap = 0;
2238 if (nobj > obj_buf_cap) {
2239 int new_cap = obj_buf_cap > 0 ? obj_buf_cap : 64;
2240 while (new_cap < nobj) new_cap *= 2;
2241 int* g1 = (int*)realloc(obj_order, (size_t)new_cap * sizeof(int));
2242 float* g2 = (float*)realloc(obj_sx_arr, (size_t)new_cap * sizeof(float));
2243 float* g3 = (float*)realloc(obj_sy_arr, (size_t)new_cap * sizeof(float));
2244 if (!g1 || !g2 || !g3) {
2245 n_log(LOG_ERR, "iso_map_draw: object buffer realloc failed");
2246 /* Keep whatever grew successfully; on next frame the
2247 * remaining one(s) will be retried. */
2248 if (g1) obj_order = g1;
2249 if (g2) obj_sx_arr = g2;
2250 if (g3) obj_sy_arr = g3;
2251 nobj = 0;
2252 } else {
2253 obj_order = g1;
2254 obj_sx_arr = g2;
2255 obj_sy_arr = g3;
2256 obj_buf_cap = new_cap;
2257 }
2258 }
2259
2260 if (nobj > 0) {
2261 for (int i = 0; i < nobj; i++) {
2262 obj_order[i] = i;
2263 objects[i].is_occluded = 0;
2264 objects[i].occlude_clip_y = 1e9f;
2265 float wsx, wsy;
2266 iso_map_to_screen_f(map, objects[i].fx, objects[i].fy, objects[i].fz, &wsx, &wsy);
2267 obj_sx_arr[i] = wsx * zoom + cam_px;
2268 obj_sy_arr[i] = wsy * zoom + cam_py;
2269 }
2270
2271 /* insertion sort by (depth, floor(fz), my, mx) to match segment draw order */
2272 for (int i = 1; i < nobj; i++) {
2273 int key = obj_order[i];
2274 int key_fz = (int)floorf(objects[key].fz);
2275 int key_my = (int)floorf(objects[key].fy);
2276 int key_mx = (int)floorf(objects[key].fx);
2277 int key_depth = key_my + key_mx;
2278 int j = i - 1;
2279 while (j >= 0) {
2280 int cj = obj_order[j];
2281 int cj_fz = (int)floorf(objects[cj].fz);
2282 int cj_my = (int)floorf(objects[cj].fy);
2283 int cj_mx = (int)floorf(objects[cj].fx);
2284 int cj_depth = cj_my + cj_mx;
2285 if (cj_depth > key_depth ||
2286 (cj_depth == key_depth && cj_fz > key_fz) ||
2287 (cj_depth == key_depth && cj_fz == key_fz && cj_my > key_my) ||
2288 (cj_depth == key_depth && cj_fz == key_fz && cj_my == key_my && cj_mx > key_mx)) {
2289 obj_order[j + 1] = obj_order[j];
2290 j--;
2291 } else {
2292 break;
2293 }
2294 }
2295 obj_order[j + 1] = key;
2296 }
2297 }
2298 n_iso_setup_us += (al_get_time() - _iso_t_setup) * 1.0e6;
2299 double _iso_t_pass1 = al_get_time();
2300 /* PASS 1: Ground segments only (bottom == 0), no objects
2301 * Draw all ground-level tiles first so they can never overdraw
2302 * elevated content (entities on bridges, floating segments, etc.).
2303 *
2304 * Wrap the entire pass in al_hold_bitmap_drawing. The previous
2305 * frame-wide wrap regressed because Pass 2 interleaves tile draws
2306 * with object-callback sprite draws (each from a different source
2307 * bitmap), forcing flushes. Pass 1 has no object callbacks, it's
2308 * pure tile draws from whatever source the caller provided. When
2309 * the caller groups tiles + transitions + overlays into one atlas,
2310 * consecutive draws share a source texture and accumulate cleanly
2311 * between state-change flushes. The internal al_set_blender calls
2312 * still flush, but runs of same-blender draws within a blender
2313 * regime now batch.
2314 *
2315 * `blender_is_one` tracks whether the current blender is
2316 * ADD/ONE/INV_ALPHA. Every set to ONE flips it true; every
2317 * set to ALPHA (walls, overlay, borders, hover, grid) flips it
2318 * false. This lets us skip the per-tile "reset to ONE" at the
2319 * head of each iteration when no ALPHA block ran in the
2320 * previous iteration, the common case for uninstrumented
2321 * ground tiles. */
2322 int blender_is_one = 1; /* set by line 2582 above the pass */
2323 /* al_hold_bitmap_drawing wrap around PASS 1, opt-in via
2324 * NILOREA_PASS1_HOLD=1. Wrapping the pass batches consecutive
2325 * bitmap draws (~2 ms speedup) but historically introduced
2326 * intermittent darker tile-mask diamond edges at the boundaries.
2327 * After the bitmap/primitive interleave carve-out (see below)
2328 * the wrap is correct end-to-end, but it remains opt-in by
2329 * environment variable for A/B perf comparison. */
2330 static int s_hold_pass1 = -1;
2331 static int s_hold_per_tile = -1;
2332 static int s_no_transitions = -1;
2333 static int s_no_overlay = -1;
2334 static int s_no_height_borders = -1;
2335 static int s_no_hover_grid = -1;
2336 static int s_tile_limit = -1;
2337 static int s_force_alpha = -1;
2338 if (s_hold_pass1 < 0) {
2339 /* Default is ON; set NILOREA_PASS1_HOLD=0 to opt OUT. */
2340 const char* e = getenv("NILOREA_PASS1_HOLD");
2341 s_hold_pass1 = (e && e[0] == '0') ? 0 : 1;
2342 }
2343 /* PASS 1 instrumentation knobs. Each is env-gated, default-off so
2344 * production builds are unchanged. Useful when investigating
2345 * rendering artefacts.
2346 *
2347 * NILOREA_PASS1_HOLD_PER_TILE=1
2348 * Replace the pass-wide al_hold_bitmap_drawing(true/false)
2349 * wrap with a per-tile cycle. Each tile becomes its own
2350 * isolated batch.
2351 *
2352 * NILOREA_PASS1_NO_TRANSITIONS=1
2353 * Skip the terrain-transitions loop entirely.
2354 *
2355 * NILOREA_PASS1_NO_OVERLAY=1
2356 * Skip the overlay-bitmap draw (ALPHA blender).
2357 *
2358 * NILOREA_PASS1_NO_HEIGHT_BORDERS=1
2359 * Skip the height-borders ALPHA draw.
2360 *
2361 * NILOREA_PASS1_NO_HOVER_GRID=1
2362 * Skip the hover-highlight and grid-overlay ALPHA draws.
2363 *
2364 * NILOREA_PASS1_TILE_LIMIT=N
2365 * Render at most N tiles in PASS 1, then break.
2366 *
2367 * NILOREA_PASS1_FORCE_ALPHA=1
2368 * Force ALPHA blender (instead of ONE) for the base-tile
2369 * and transitions draws. */
2370 if (s_hold_per_tile < 0) {
2371 const char* e = getenv("NILOREA_PASS1_HOLD_PER_TILE");
2372 s_hold_per_tile = (e && e[0] == '1') ? 1 : 0;
2373 }
2374 if (s_no_transitions < 0) {
2375 const char* e = getenv("NILOREA_PASS1_NO_TRANSITIONS");
2376 s_no_transitions = (e && e[0] == '1') ? 1 : 0;
2377 }
2378 if (s_no_overlay < 0) {
2379 const char* e = getenv("NILOREA_PASS1_NO_OVERLAY");
2380 s_no_overlay = (e && e[0] == '1') ? 1 : 0;
2381 }
2382 if (s_no_height_borders < 0) {
2383 const char* e = getenv("NILOREA_PASS1_NO_HEIGHT_BORDERS");
2384 s_no_height_borders = (e && e[0] == '1') ? 1 : 0;
2385 }
2386 if (s_no_hover_grid < 0) {
2387 const char* e = getenv("NILOREA_PASS1_NO_HOVER_GRID");
2388 s_no_hover_grid = (e && e[0] == '1') ? 1 : 0;
2389 }
2390 if (s_tile_limit < 0) {
2391 const char* e = getenv("NILOREA_PASS1_TILE_LIMIT");
2392 s_tile_limit = (e && e[0] >= '0' && e[0] <= '9') ? atoi(e) : 0;
2393 }
2394 if (s_force_alpha < 0) {
2395 const char* e = getenv("NILOREA_PASS1_FORCE_ALPHA");
2396 s_force_alpha = (e && e[0] == '1') ? 1 : 0;
2397 }
2398 /* If per-tile hold is requested, force the pass-wide hold OFF:
2399 * the per-tile cycle below opens/closes its own holds. */
2400 int effective_hold_pass = s_hold_pass1 && !s_hold_per_tile;
2401 if (effective_hold_pass) {
2402 al_hold_bitmap_drawing(true);
2403 /* Batch warmup. The first batched draw after
2404 * al_hold_bitmap_drawing(true) can hit a slightly different
2405 * fragment-shading path than subsequent draws within the same
2406 * held context, producing darker fractional-alpha tile-mask
2407 * edges on the early painter-order tiles.
2408 *
2409 * Defence: prepend a fully-transparent 1-pixel draw at an
2410 * offscreen position so the suspect "first draw in hold
2411 * cycle" is absorbed into an invisible pixel. The actual
2412 * tile draws then all run from the second-draw-onwards
2413 * state, which is the consistent one. Zero visual cost
2414 * (transparent + offscreen) and negligible perf cost. */
2415 if (tile_bitmaps[0]) {
2416 al_draw_tinted_scaled_bitmap(tile_bitmaps[0],
2417 al_map_rgba(0, 0, 0, 0),
2418 0, 0, 1, 1,
2419 -100, -100, 1, 1, 0);
2420 }
2421 }
2422 int _p1_tiles_drawn = 0;
2423 for (int di = 0; di < num_entries; di++) {
2424 if (draw_order[di].bottom > 0) continue; /* skip elevated */
2425
2426 /* Tile limit knob: stop after N ground tiles drawn. */
2427 if (s_tile_limit > 0 && _p1_tiles_drawn >= s_tile_limit) break;
2428 _p1_tiles_drawn++;
2429
2430 /* Per-tile hold cycle: each tile is its own isolated batch.
2431 * The pass-wide hold is suppressed when this knob is on. */
2432 if (s_hold_per_tile) al_hold_bitmap_drawing(true);
2433
2434 int mx = draw_order[di].mx;
2435 int my = draw_order[di].my;
2436 int seg_top = draw_order[di].top;
2437 int h = seg_top;
2438 int base = iso_map_get_terrain(map, mx, my);
2439
2440 /* `al_hold_bitmap_drawing` only batches bitmap draws.
2441 * `al_draw_line` / `al_draw_filled_polygon` (primitives addon)
2442 * draw IMMEDIATELY regardless of hold. So under the pass-wide
2443 * hold:
2444 * - tile base bitmap is queued in batch
2445 * - wall polygon is drawn now at the screen position
2446 * - border line is drawn now
2447 * - more tiles ...
2448 * - end of pass: hold(false) flushes all queued bitmaps
2449 *
2450 * The bitmap flush at end draws on TOP of the primitives,
2451 * hiding height borders / wall edges. Adjacent tiles' diamond
2452 * edges (where transitions + border lines render at the same
2453 * sub-pixel position with bilinear vs line-AA coverage) also
2454 * produce a doubled-outline effect during camera motion.
2455 *
2456 * Fix: for tiles that need primitive draws (elevated walls,
2457 * height borders, hover highlight, grid overlay), step OUT of
2458 * the held batch: flush the queued bitmaps, draw the tile
2459 * fully in immediate mode (bitmaps + primitives interleaved
2460 * correctly), then re-enter the held batch for the next
2461 * batchable tile. The bulk of tiles (flat ground, no overlay,
2462 * no hover, no grid) stays inside the held batch unchanged. */
2463 /* Smooth-mode slope detection, computed HERE, before the hold-batch
2464 * decision below, because a sloped tile's top is drawn with
2465 * al_draw_prim (a primitives-addon call) which renders INCORRECTLY
2466 * inside al_hold_bitmap_drawing(true): the queued-batch state scales
2467 * + misplaces the warped quad (the "flat<->slope transition tiles black
2468 * out / shrink toward screen centre" bug). Such tiles must step out
2469 * of the held batch exactly like wall/hover/grid tiles already do. A
2470 * FLAT tile takes the batch-safe al_draw_tinted_scaled_bitmap path and
2471 * stays held. h_n/h_e/h_s/h_w are reused by the vx/vy block below so
2472 * iso_map_smooth_corner_heights is called only once per tile. */
2473 float h_n = (float)h, h_e = (float)h, h_s = (float)h, h_w = (float)h;
2474 int tile_is_flat = 1;
2475 if (map->smooth_height) {
2476 iso_map_smooth_corner_heights(map, mx, my, &h_n, &h_e, &h_s, &h_w);
2477 tile_is_flat = (h_n == h_e && h_e == h_s && h_s == h_w);
2478 }
2479 int tile_needs_primitives = (seg_top > 0) ||
2480 (map->smooth_height && !tile_is_flat) ||
2481 (!s_no_hover_grid && mx == map->hover_mx && my == map->hover_my) ||
2482 (map->show_grid && !s_no_hover_grid);
2483 int unbatched_tile = effective_hold_pass && tile_needs_primitives;
2484 if (unbatched_tile) {
2485 al_hold_bitmap_drawing(false);
2486 }
2487
2488 /* Per-segment tile overrides: upper_tile for top face, lower_tile for walls */
2489 int top_terrain = base;
2490 int wall_terrain = base;
2491 {
2492 int si = draw_order[di].seg_idx;
2493 int tidx = my * map->width + mx;
2494 if (map->segments && tidx >= 0 && tidx < map->width * map->height &&
2495 si >= 0 && si < map->segments[tidx].count) {
2496 int ut = map->segments[tidx].segs[si].upper_tile;
2497 int lt = map->segments[tidx].segs[si].lower_tile;
2498 if (ut >= 0 && ut < map->num_terrains) top_terrain = ut;
2499 if (lt >= 0 && lt < map->num_terrains) wall_terrain = lt;
2500 }
2501 }
2502
2503 float vx[4], vy[4];
2504 float base_dx, base_dy;
2505
2506 if (map->smooth_height) {
2507 /* h_n/h_e/h_s/h_w + tile_is_flat already computed above. */
2508 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, h_n, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2509 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, h_e, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2510 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, h_s, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2511 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, h_w, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2512 base_dx = vx[3];
2513 base_dy = vy[0];
2514 } else {
2515 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, (float)h, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2516 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2517 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2518 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2519 base_dx = vx[3];
2520 base_dy = vy[0];
2521 }
2522
2523 /* Culling */
2524 float min_vy = fminf(fminf(vy[0], vy[1]), fminf(vy[2], vy[3]));
2525 float max_vy = fmaxf(fmaxf(vy[0], vy[1]), fmaxf(vy[2], vy[3]));
2526 float max_side = fmaxf((float)h, (float)map->max_height) * lift;
2527 if (vx[1] < 0 || vx[3] > (float)screen_w) continue;
2528 if (max_vy + max_side < 0 || min_vy > (float)screen_h) continue;
2529
2530 /* Height-based tint + ambient + dynamic lighting for this segment */
2531 float amb_r = map->ambient_r > 0.0f ? map->ambient_r : 1.0f;
2532 float amb_g = map->ambient_g > 0.0f ? map->ambient_g : 1.0f;
2533 float amb_b = map->ambient_b > 0.0f ? map->ambient_b : 1.0f;
2534 float dyn_r = 0.0f, dyn_g = 0.0f, dyn_b = 0.0f;
2535 if (map->dynamic_light_map) {
2536 int lidx = (my * map->width + mx) * 3;
2537 dyn_r = map->dynamic_light_map[lidx];
2538 dyn_g = map->dynamic_light_map[lidx + 1];
2539 dyn_b = map->dynamic_light_map[lidx + 2];
2540 }
2541 ALLEGRO_COLOR seg_tint = _compute_tile_tint(seg_top, map->max_height,
2543 amb_r, amb_g, amb_b,
2544 dyn_r, dyn_g, dyn_b);
2545 float seg_brightness = 1.0f;
2546 if (map->height_tint_intensity > 0.0f && map->max_height > 0) {
2547 float ratio = (float)seg_top / (float)map->max_height;
2548 seg_brightness = 1.0f - map->height_tint_intensity * (1.0f - ratio);
2549 }
2550 /* Factor in ambient for wall brightness */
2551 seg_brightness *= (amb_r + amb_g + amb_b) / 3.0f;
2552 float wall_dyn = (dyn_r + dyn_g + dyn_b) / 3.0f;
2553 seg_brightness += wall_dyn;
2554 if (seg_brightness > 1.0f) seg_brightness = 1.0f;
2555
2556 /* Top surface: ALWAYS set the blender at the start of every
2557 * tile, dropping any cross-tile cache. Under
2558 * `al_hold_bitmap_drawing(true)`, `al_set_blender` flushes the
2559 * current held batch and starts a new one. If the call were
2560 * skipped when the previous tile ended on ONE, the held
2561 * batch's flush boundaries would depend on which tiles had
2562 * overlays/walls/hover/grid, and that set shifts when the
2563 * camera moves. Inconsistent flush boundaries cause
2564 * inconsistent GPU-rasterised pixel coverage on shared
2565 * diamond edges, producing flickering darker borders during
2566 * camera motion. Forcing the call here makes the flush
2567 * boundary deterministic at every tile, trading O(1000) extra
2568 * al_set_blender calls per frame for stable rasterisation.
2569 * The within-tile transitions loop keeps its conditional `if
2570 * (!blender_is_one)` skip because within a tile the state is
2571 * fresh and skipping is safe. */
2572 if (s_force_alpha) {
2573 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2574 blender_is_one = 0;
2575 } else {
2576 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2577 blender_is_one = 1;
2578 }
2579 if (map->smooth_height && !tile_is_flat) {
2580 _draw_tile_warped(tile_bitmaps[top_terrain], vx, vy, tile_w, tile_h, seg_tint);
2581 } else {
2582 /* Flat tile (or CUT mode): the al_draw_prim warp samples atlas
2583 * sub-bitmaps differently from al_draw_tinted_scaled_bitmap, which
2584 * renders flat ground tiles wrong once tiles are packed into the
2585 * terrain atlas. For a flat tile the warp geometry is identical to
2586 * this scaled-bitmap draw anyway (verified: corner span == tdw*tdh),
2587 * so route flat tiles through the proven atlas-correct path and
2588 * reserve the warp for genuinely sloped tiles. */
2589 al_draw_tinted_scaled_bitmap(tile_bitmaps[top_terrain], seg_tint,
2590 0, 0, (float)tile_w, (float)tile_h,
2591 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2592 }
2593
2594 /* Cliff walls (internally sets ONE blender). */
2595 if (seg_top > 0) {
2596 if (map->smooth_height) {
2597 int h_east = iso_map_get_height(map, mx + 1, my);
2598 int h_south = iso_map_get_height(map, mx, my + 1);
2599 int h_se = iso_map_get_height(map, mx + 1, my + 1);
2600 if ((h - h_east > map->smooth_slope_max) ||
2601 (h - h_south > map->smooth_slope_max) ||
2602 (h - h_se > map->smooth_slope_max)) {
2603 float ce_x, ce_y, cs_x, cs_y, cw_x, cw_y;
2604 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &ce_x, &ce_y);
2605 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &cs_x, &cs_y);
2606 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &cw_x, &cw_y);
2607 _draw_segment_sides(map, ce_x, ce_y, cs_x, cs_y, cw_x, cw_y,
2608 mx, my, 0, seg_top, lift, seg_brightness, wall_terrain);
2609 /* blender_is_one stays 1: function leaves state on ONE */
2610 }
2611 } else {
2612 _draw_segment_sides(map, vx[1], vy[1], vx[2], vy[2], vx[3], vy[3],
2613 mx, my, 0, seg_top, lift, seg_brightness, wall_terrain);
2614 /* blender_is_one stays 1 */
2615 }
2616 }
2617
2618 /* Terrain transitions (same tint as base tile; ONE blender) */
2619 if (transition_tiles && !s_no_transitions) {
2620 int edge_bits_arr[16];
2621 int corner_bits_arr[16];
2622 int nt = map->num_terrains < 16 ? map->num_terrains : 16;
2623 memset(edge_bits_arr, 0, sizeof(int) * (size_t)nt);
2624 memset(corner_bits_arr, 0, sizeof(int) * (size_t)nt);
2625 iso_map_calc_transitions_full(map, mx, my, edge_bits_arr, corner_bits_arr);
2626
2627 for (int t = base + 1; t < nt; t++) {
2628 if (edge_bits_arr[t] != 0 && transition_tiles[t] &&
2629 transition_tiles[t][edge_bits_arr[t]] != NULL) {
2630 if (!blender_is_one && !s_force_alpha) {
2631 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2632 blender_is_one = 1;
2633 }
2634 if (map->smooth_height && !tile_is_flat) {
2635 _draw_tile_warped(transition_tiles[t][edge_bits_arr[t]],
2636 vx, vy, tile_w, tile_h, seg_tint);
2637 } else {
2638 al_draw_tinted_scaled_bitmap(transition_tiles[t][edge_bits_arr[t]],
2639 seg_tint,
2640 0, 0, (float)tile_w, (float)tile_h,
2641 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2642 }
2643 }
2644
2645 int cidx = num_edge_masks + corner_bits_arr[t];
2646 if (corner_bits_arr[t] != 0 && cidx < num_masks &&
2647 transition_tiles[t] && transition_tiles[t][cidx] != NULL) {
2648 if (!blender_is_one && !s_force_alpha) {
2649 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2650 blender_is_one = 1;
2651 }
2652 if (map->smooth_height && !tile_is_flat) {
2654 vx, vy, tile_w, tile_h, seg_tint);
2655 } else {
2656 al_draw_tinted_scaled_bitmap(transition_tiles[t][cidx],
2657 seg_tint,
2658 0, 0, (float)tile_w, (float)tile_h,
2659 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2660 }
2661 }
2662 }
2663 }
2664
2665 /* Overlay tile (drawn after terrain transitions, before
2666 * walls/borders). Overlay bitmaps are premultiplied (loaded
2667 * through the same path as base tiles + transitions), so
2668 * ALPHA blending would produce quadratic darkening at
2669 * fractional-alpha edges. Use ONE/INVERSE_ALPHA which matches
2670 * the premul source format. Also avoids cross-tile
2671 * contamination under al_hold_bitmap_drawing(true), where
2672 * al_set_blender does not flush the held batch mid-pass: an
2673 * ALPHA blender set here would otherwise apply to draws
2674 * emitted earlier in the batch by other tiles when the batch
2675 * eventually flushes. */
2676 if (overlay_bitmaps && map->overlay && num_overlay_tiles > 0 && !s_no_overlay) {
2677 int ov_idx = my * map->width + mx;
2678 int ov_tile = map->overlay[ov_idx];
2679 if (ov_tile > 0 && ov_tile < num_overlay_tiles && overlay_bitmaps[ov_tile]) {
2680 if (!blender_is_one) {
2681 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2682 blender_is_one = 1;
2683 }
2684 if (map->smooth_height && !tile_is_flat) {
2685 _draw_tile_warped(overlay_bitmaps[ov_tile], vx, vy, tile_w, tile_h, seg_tint);
2686 } else {
2687 al_draw_tinted_scaled_bitmap(overlay_bitmaps[ov_tile], seg_tint,
2688 0, 0, (float)tile_w, (float)tile_h,
2689 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2690 }
2691 }
2692 }
2693
2694 /* Height borders (ONE blender; lines use RGB=0 so premul == straight) */
2695 if (seg_top > 0 && !s_no_height_borders) {
2696 if (!blender_is_one) {
2697 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2698 blender_is_one = 1;
2699 }
2700 _draw_height_borders(map, mx, my, draw_order[di].seg_idx,
2701 0, seg_top,
2702 draw_order[di].underside,
2703 vx, vy, lift);
2704 }
2705
2706 /* Hover highlight (ONE blender; colors premultiplied below) */
2707 if (!s_no_hover_grid && mx == map->hover_mx && my == map->hover_my) {
2708 if (!blender_is_one) {
2709 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2710 blender_is_one = 1;
2711 }
2712 /* Premul: each RGB channel scaled by alpha/255. */
2713 ALLEGRO_COLOR hc = player_mode
2714 ? al_map_rgba(100 * 120 / 255, 255 * 120 / 255, 100 * 120 / 255, 120)
2715 : al_map_rgba(255 * 120 / 255, 255 * 120 / 255, 100 * 120 / 255, 120);
2716 float verts[8] = {vx[0], vy[0], vx[1], vy[1],
2717 vx[2], vy[2], vx[3], vy[3]};
2718 al_draw_filled_polygon(verts, 4, hc);
2719 ALLEGRO_COLOR oc = player_mode
2720 ? al_map_rgba(100 * 200 / 255, 255 * 200 / 255, 100 * 200 / 255, 200)
2721 : al_map_rgba(255 * 200 / 255, 255 * 200 / 255, 100 * 200 / 255, 200);
2722 al_draw_line(vx[0], vy[0], vx[1], vy[1], oc, 1.5f);
2723 al_draw_line(vx[1], vy[1], vx[2], vy[2], oc, 1.5f);
2724 al_draw_line(vx[2], vy[2], vx[3], vy[3], oc, 1.5f);
2725 al_draw_line(vx[3], vy[3], vx[0], vy[0], oc, 1.5f);
2726 }
2727
2728 /* Grid overlay (ONE blender; colors premultiplied below).
2729 * No blender_is_one = 1 update needed: this is the last block in
2730 * the iteration that touches the blender, and the next iteration
2731 * unconditionally re-sets the blender at the top of the loop. */
2732 if (map->show_grid && !s_no_hover_grid) {
2733 if (!blender_is_one) {
2734 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2735 }
2736 /* Premul: white 60/255 alpha is (60,60,60,60). */
2737 ALLEGRO_COLOR gc = al_map_rgba(60, 60, 60, 60);
2738 al_draw_line(vx[0], vy[0], vx[1], vy[1], gc, 1.0f);
2739 al_draw_line(vx[1], vy[1], vx[2], vy[2], gc, 1.0f);
2740 al_draw_line(vx[2], vy[2], vx[3], vy[3], gc, 1.0f);
2741 al_draw_line(vx[3], vy[3], vx[0], vy[0], gc, 1.0f);
2742
2743 if (h > 0) {
2744 unsigned char a2 = (unsigned char)(60 + h * 35);
2745 /* Premul: (R*a/255, G*a/255, B*a/255, a) for (255,200,100,a). */
2746 ALLEGRO_COLOR hc2 = al_map_rgba(a2,
2747 (unsigned char)(200 * a2 / 255),
2748 (unsigned char)(100 * a2 / 255),
2749 a2);
2750 float dot_x = (vx[0] + vx[2]) / 2.0f;
2751 float dot_y = (vy[0] + vy[1] + vy[2] + vy[3]) / 4.0f;
2752 al_draw_filled_circle(dot_x, dot_y, 2.0f * zoom, hc2);
2753 }
2754 }
2755
2756 /* Per-tile hold cycle: flush this tile's batch before moving
2757 * on. Each tile becomes its own VBO. */
2758 if (s_hold_per_tile) al_hold_bitmap_drawing(false);
2759
2760 /* Re-enter the held batch for the next batchable tile.
2761 * Resets the blender to ONE for the next tile's base bitmap,
2762 * since immediate-mode primitive draws inside the unbatched
2763 * section may have left other state.
2764 * No blender_is_one = 1 update: the next iteration unconditionally
2765 * re-sets the blender at the top of the loop. */
2766 if (unbatched_tile) {
2767 al_hold_bitmap_drawing(true);
2768 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2769 }
2770 }
2771
2772 if (effective_hold_pass) al_hold_bitmap_drawing(false);
2773 n_iso_pass1_us += (al_get_time() - _iso_t_pass1) * 1.0e6;
2774 double _iso_t_pass2 = al_get_time();
2775 /* PASS 2: Elevated segments + ALL objects (interleaved)
2776 * Objects are interleaved at every draw_order entry (ground and
2777 * elevated) to preserve correct depth ordering. Only elevated
2778 * segments (bottom > 0) are actually rendered, ground segments
2779 * were already drawn in Pass 1. */
2780 int obj_cursor = 0;
2781 for (int di = 0; di < num_entries; di++) {
2782 int mx = draw_order[di].mx;
2783 int my = draw_order[di].my;
2784 int seg_bottom = draw_order[di].bottom;
2785
2786 /* Object interleaving runs at every entry regardless of
2787 * ground/elevated, this ensures objects at ground-level depth
2788 * positions are drawn at the correct point in the sort order. */
2789 if (nobj > 0) {
2790 int seg_depth = my + mx;
2791 while (obj_cursor < nobj) {
2792 int oi = obj_order[obj_cursor];
2793 int obj_fz = (int)floorf(objects[oi].fz);
2794 int obj_my = (int)floorf(objects[oi].fy);
2795 int obj_mx = (int)floorf(objects[oi].fx);
2796 int obj_depth = obj_my + obj_mx;
2797 if (obj_depth > seg_depth ||
2798 (obj_depth == seg_depth && obj_fz > seg_bottom) ||
2799 (obj_depth == seg_depth && obj_fz == seg_bottom && obj_my > my) ||
2800 (obj_depth == seg_depth && obj_fz == seg_bottom && obj_my == my && obj_mx >= mx))
2801 break;
2802 if (objects[oi].draw) {
2803 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
2804 zoom, 1.0f, objects[oi].user_data);
2805 }
2806 obj_cursor++;
2807 }
2808 }
2809
2810 /* Ground segments already drawn in Pass 1, skip rendering */
2811 if (seg_bottom == 0) continue;
2812
2813 /* Elevated segment rendering */
2814 int seg_top = draw_order[di].top;
2815 int h = seg_top;
2816 int base = iso_map_get_terrain(map, mx, my);
2817
2818 /* Per-segment tile overrides for elevated segments */
2819 int etop_terrain = base;
2820 int ewall_terrain = base;
2821 {
2822 int si = draw_order[di].seg_idx;
2823 int tidx = my * map->width + mx;
2824 if (map->segments && tidx >= 0 && tidx < map->width * map->height &&
2825 si >= 0 && si < map->segments[tidx].count) {
2826 int ut = map->segments[tidx].segs[si].upper_tile;
2827 int lt = map->segments[tidx].segs[si].lower_tile;
2828 if (ut >= 0 && ut < map->num_terrains) etop_terrain = ut;
2829 if (lt >= 0 && lt < map->num_terrains) ewall_terrain = lt;
2830 }
2831 }
2832
2833 float vx[4], vy[4];
2834 float base_dx, base_dy;
2835 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, (float)h, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2836 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2837 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2838 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2839 base_dx = vx[3];
2840 base_dy = vy[0];
2841
2842 /* Culling, objects already handled above, safe to continue */
2843 float min_vy = fminf(fminf(vy[0], vy[1]), fminf(vy[2], vy[3]));
2844 float max_vy = fmaxf(fmaxf(vy[0], vy[1]), fmaxf(vy[2], vy[3]));
2845 float max_side = fmaxf((float)h, (float)map->max_height) * lift;
2846 if (vx[1] < 0 || vx[3] > (float)screen_w) continue;
2847 if (max_vy + max_side < 0 || min_vy > (float)screen_h) continue;
2848
2849 /* Underside face for floating segments */
2850 if (draw_order[di].underside && tile_bitmaps[etop_terrain]) {
2851 float under_sx, under_sy;
2852 iso_map_to_screen(map, mx, my, seg_bottom, &under_sx, &under_sy);
2853 float under_dx = under_sx * zoom + cam_px;
2854 float under_dy = under_sy * zoom + cam_py;
2855 _draw_segment_underside(tile_bitmaps[etop_terrain], under_dx, under_dy,
2856 phw, phh, tile_draw_w, tile_draw_h,
2857 tile_w, tile_h);
2858 }
2859
2860 /* Height-based tint for elevated segment */
2861 /* Elevated segment: same ambient + dynamic light from base tile */
2862 float eamb_r = map->ambient_r > 0.0f ? map->ambient_r : 1.0f;
2863 float eamb_g = map->ambient_g > 0.0f ? map->ambient_g : 1.0f;
2864 float eamb_b = map->ambient_b > 0.0f ? map->ambient_b : 1.0f;
2865 float edyn_r = 0.0f, edyn_g = 0.0f, edyn_b = 0.0f;
2866 if (map->dynamic_light_map) {
2867 int elidx = (my * map->width + mx) * 3;
2868 edyn_r = map->dynamic_light_map[elidx];
2869 edyn_g = map->dynamic_light_map[elidx + 1];
2870 edyn_b = map->dynamic_light_map[elidx + 2];
2871 }
2872 ALLEGRO_COLOR eseg_tint = _compute_tile_tint(seg_top, map->max_height,
2874 eamb_r, eamb_g, eamb_b,
2875 edyn_r, edyn_g, edyn_b);
2876 float eseg_brightness = 1.0f;
2877 if (map->height_tint_intensity > 0.0f && map->max_height > 0) {
2878 float ratio = (float)seg_top / (float)map->max_height;
2879 eseg_brightness = 1.0f - map->height_tint_intensity * (1.0f - ratio);
2880 }
2881 eseg_brightness *= (eamb_r + eamb_g + eamb_b) / 3.0f;
2882 eseg_brightness += (edyn_r + edyn_g + edyn_b) / 3.0f;
2883 if (eseg_brightness > 1.0f) eseg_brightness = 1.0f;
2884
2885 /* Top surface (flat, elevated segments don't use smooth height) */
2886 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2887 al_draw_tinted_scaled_bitmap(tile_bitmaps[etop_terrain], eseg_tint,
2888 0, 0, (float)tile_w, (float)tile_h,
2889 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2890
2891 /* Cliff walls + height borders */
2892 if (seg_top > seg_bottom) {
2893 _draw_segment_sides(map, vx[1], vy[1], vx[2], vy[2], vx[3], vy[3],
2894 mx, my, seg_bottom, seg_top, lift, eseg_brightness, ewall_terrain);
2895 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2896 _draw_height_borders(map, mx, my, draw_order[di].seg_idx,
2897 seg_bottom, seg_top,
2898 draw_order[di].underside,
2899 vx, vy, lift);
2900 }
2901
2902 /* Hover highlight */
2903 if (mx == map->hover_mx && my == map->hover_my) {
2904 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2905 ALLEGRO_COLOR hc = player_mode
2906 ? al_map_rgba(100, 255, 100, 120)
2907 : al_map_rgba(255, 255, 100, 120);
2908 float verts[8] = {vx[0], vy[0], vx[1], vy[1],
2909 vx[2], vy[2], vx[3], vy[3]};
2910 al_draw_filled_polygon(verts, 4, hc);
2911 ALLEGRO_COLOR oc = player_mode
2912 ? al_map_rgba(100, 255, 100, 200)
2913 : al_map_rgba(255, 255, 100, 200);
2914 al_draw_line(vx[0], vy[0], vx[1], vy[1], oc, 1.5f);
2915 al_draw_line(vx[1], vy[1], vx[2], vy[2], oc, 1.5f);
2916 al_draw_line(vx[2], vy[2], vx[3], vy[3], oc, 1.5f);
2917 al_draw_line(vx[3], vy[3], vx[0], vy[0], oc, 1.5f);
2918 }
2919
2920 /* Grid overlay */
2921 if (map->show_grid) {
2922 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2923 ALLEGRO_COLOR gc = al_map_rgba(255, 255, 255, 60);
2924 al_draw_line(vx[0], vy[0], vx[1], vy[1], gc, 1.0f);
2925 al_draw_line(vx[1], vy[1], vx[2], vy[2], gc, 1.0f);
2926 al_draw_line(vx[2], vy[2], vx[3], vy[3], gc, 1.0f);
2927 al_draw_line(vx[3], vy[3], vx[0], vy[0], gc, 1.0f);
2928
2929 if (h > 0) {
2930 ALLEGRO_COLOR hc2 = al_map_rgba(255, 200, 100,
2931 (unsigned char)(60 + h * 35));
2932 float dot_x = (vx[0] + vx[2]) / 2.0f;
2933 float dot_y = (vy[0] + vy[1] + vy[2] + vy[3]) / 4.0f;
2934 al_draw_filled_circle(dot_x, dot_y, 2.0f * zoom, hc2);
2935 }
2936 }
2937 }
2938
2939 /* Draw any remaining objects past the last entry */
2940 while (obj_cursor < nobj) {
2941 int oi = obj_order[obj_cursor];
2942 if (objects[oi].draw) {
2943 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
2944 zoom, 1.0f, objects[oi].user_data);
2945 }
2946 obj_cursor++;
2947 }
2948
2949 n_iso_pass2_us += (al_get_time() - _iso_t_pass2) * 1.0e6;
2950 double _iso_t_pass3 = al_get_time();
2951 /* Step 7: Occlusion detection and clipped overlay pass.
2952 * For each occluded entity, restrict the ghost draw to the region
2953 * below the occluding tile's north vertex using a clipping rectangle. */
2954 for (int i = 0; i < nobj; i++) {
2955 int oi = obj_order[i];
2956 if (objects[oi].occluded_alpha <= 0.0f) continue;
2957 if (!objects[oi].draw) continue;
2958
2959 float clip_y = 1e9f;
2961 objects[oi].fx, objects[oi].fy, objects[oi].fz,
2962 obj_sx_arr[oi], obj_sy_arr[oi],
2963 objects[oi].sprite_h,
2964 objects[oi].sprite_half_w,
2965 cam_px, cam_py, zoom,
2966 &clip_y))
2967 continue;
2968
2969 objects[oi].is_occluded = 1;
2970 objects[oi].occlude_clip_y = clip_y;
2971
2972 /* Clip ghost overlay to below the occluder's top edge. The
2973 * ghost is drawn at alpha < 1 to make the occluded sprite
2974 * look semi-transparent. Use straight-alpha blending here:
2975 * with the premultiplied (ADD/ONE/INV_ALPHA) blender the
2976 * 0.4-alpha overlay is ADDED on top of the already-drawn
2977 * full-bright sprite, brightening it instead of revealing
2978 * what's behind. ALPHA/INV_ALPHA does the standard
2979 * src*alpha + dst*(1-alpha) transparent compositing the
2980 * caller meant. */
2981 int prev_cx, prev_cy, prev_cw, prev_ch;
2982 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
2983 int clip_top = (int)floorf(clip_y);
2984 if (clip_top < prev_cy) clip_top = prev_cy;
2985 al_set_clipping_rectangle(prev_cx, clip_top,
2986 prev_cw, prev_cy + prev_ch - clip_top);
2987 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2988 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
2989 zoom, objects[oi].occluded_alpha, objects[oi].user_data);
2990 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
2991 }
2992
2993 n_iso_pass3_us += (al_get_time() - _iso_t_pass3) * 1.0e6;
2994
2995 /* The draw_order / obj_* buffers are file-static and reused
2996 * across calls; no per-call free. They survive for the process
2997 * lifetime which is fine: max draw entries is bounded by
2998 * map->width * map->height * ISO_MAX_SEGMENTS_PER_TILE (~2 KB
2999 * for typical 32x32x2 chunks), and obj_buf_cap grows once to the
3000 * scene's peak entity count. */
3001} /* iso_map_draw() */
3002
3003/* Transition mask & tile generation (Article 934) */
3004
3012void iso_mask_tile_to_diamond(ALLEGRO_BITMAP* bmp, int tile_w, int tile_h) {
3013 __n_assert(bmp, return);
3014
3015 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(bmp,
3016 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_READWRITE);
3017 if (!lr) return;
3018
3019 for (int py = 0; py < tile_h; py++) {
3020 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3021 for (int px = 0; px < tile_w; px++) {
3022 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3023 row[px * 4 + 0] = 0;
3024 row[px * 4 + 1] = 0;
3025 row[px * 4 + 2] = 0;
3026 row[px * 4 + 3] = 0;
3027 }
3028 }
3029 }
3030 al_unlock_bitmap(bmp);
3031}
3032
3043void iso_generate_transition_masks(ALLEGRO_BITMAP** masks, int tile_w, int tile_h) {
3044 __n_assert(masks, return);
3045
3046 float cx = (float)tile_w / 2.0f;
3047 float cy = (float)tile_h / 2.0f;
3048
3049 ALLEGRO_STATE state;
3050 al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
3051
3052 /* Edge masks (0..15) */
3053 for (int mask = 0; mask < ISO_NUM_EDGE_MASKS; mask++) {
3054 masks[mask] = al_create_bitmap(tile_w, tile_h);
3055 al_set_target_bitmap(masks[mask]);
3056 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3057
3058 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(masks[mask],
3059 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_WRITEONLY);
3060 if (!lr) continue;
3061
3062 for (int py = 0; py < tile_h; py++) {
3063 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3064 for (int px = 0; px < tile_w; px++) {
3065 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3066 row[px * 4 + 0] = 0;
3067 row[px * 4 + 1] = 0;
3068 row[px * 4 + 2] = 0;
3069 row[px * 4 + 3] = 0;
3070 continue;
3071 }
3072
3073 float alpha = 0.0f;
3074 float nx = ((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
3075 float ny = ((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
3076 float gw = 0.65f;
3077
3078 if (mask & ISO_EDGE_W) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (-nx - ny) / gw)));
3079 if (mask & ISO_EDGE_N) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (nx - ny) / gw)));
3080 if (mask & ISO_EDGE_E) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (nx + ny) / gw)));
3081 if (mask & ISO_EDGE_S) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (-nx + ny) / gw)));
3082
3083 if (alpha > 1.0f) alpha = 1.0f;
3084 alpha = alpha * alpha * (3.0f - 2.0f * alpha);
3085
3086 unsigned char a = (unsigned char)(alpha * 255.0f);
3087 row[px * 4 + 0] = a;
3088 row[px * 4 + 1] = a;
3089 row[px * 4 + 2] = a;
3090 row[px * 4 + 3] = a;
3091 }
3092 }
3093 al_unlock_bitmap(masks[mask]);
3094 }
3095
3096 /* Corner masks (16..31)
3097 * In isometric diamond projection, diagonal neighbors map to diamond tips:
3098 * NW (mx-1,my-1) = North tip (0, -1) in normalized coords
3099 * NE (mx+1,my-1) = East tip (1, 0)
3100 * SE (mx+1,my+1) = South tip (0, 1)
3101 * SW (mx-1,my+1) = West tip (-1, 0) */
3102 for (int mask = 0; mask < ISO_NUM_CORNER_MASKS; mask++) {
3103 int idx = ISO_NUM_EDGE_MASKS + mask;
3104 masks[idx] = al_create_bitmap(tile_w, tile_h);
3105 al_set_target_bitmap(masks[idx]);
3106 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3107
3108 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(masks[idx],
3109 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_WRITEONLY);
3110 if (!lr) continue;
3111
3112 for (int py = 0; py < tile_h; py++) {
3113 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3114 for (int px = 0; px < tile_w; px++) {
3115 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3116 row[px * 4 + 0] = 0;
3117 row[px * 4 + 1] = 0;
3118 row[px * 4 + 2] = 0;
3119 row[px * 4 + 3] = 0;
3120 continue;
3121 }
3122
3123 float alpha = 0.0f;
3124 float nx = ((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
3125 float ny = ((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
3126 float corner_radius = 0.55f;
3127
3128 if (mask & ISO_CORNER_NW) {
3129 float dy = ny + 1.0f;
3130 float dist = sqrtf(nx * nx + dy * dy);
3131 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3132 alpha = fmaxf(alpha, c_alpha);
3133 }
3134 if (mask & ISO_CORNER_NE) {
3135 float dx = nx - 1.0f;
3136 float dist = sqrtf(dx * dx + ny * ny);
3137 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3138 alpha = fmaxf(alpha, c_alpha);
3139 }
3140 if (mask & ISO_CORNER_SE) {
3141 float dy = ny - 1.0f;
3142 float dist = sqrtf(nx * nx + dy * dy);
3143 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3144 alpha = fmaxf(alpha, c_alpha);
3145 }
3146 if (mask & ISO_CORNER_SW) {
3147 float dx = nx + 1.0f;
3148 float dist = sqrtf(dx * dx + ny * ny);
3149 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3150 alpha = fmaxf(alpha, c_alpha);
3151 }
3152
3153 if (alpha > 1.0f) alpha = 1.0f;
3154 alpha = alpha * alpha * (3.0f - 2.0f * alpha);
3155
3156 unsigned char a = (unsigned char)(alpha * 255.0f);
3157 row[px * 4 + 0] = a;
3158 row[px * 4 + 1] = a;
3159 row[px * 4 + 2] = a;
3160 row[px * 4 + 3] = a;
3161 }
3162 }
3163 al_unlock_bitmap(masks[idx]);
3164 }
3165
3166 al_restore_state(&state);
3167 n_log(LOG_INFO, "Generated %d transition masks (%dx%d)", ISO_NUM_MASKS, tile_w, tile_h);
3168}
3169
3174void iso_generate_transition_tiles(ALLEGRO_BITMAP*** tiles,
3175 ALLEGRO_BITMAP** masks,
3176 ALLEGRO_BITMAP** tile_bitmaps,
3177 int num_terrains,
3178 int tile_w,
3179 int tile_h) {
3180 __n_assert(tiles, return);
3181 __n_assert(masks, return);
3182 __n_assert(tile_bitmaps, return);
3183
3184 ALLEGRO_STATE state;
3185 al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
3186
3187 for (int t = 0; t < num_terrains; t++) {
3188 for (int m = 0; m < ISO_NUM_MASKS; m++) {
3189 /* Skip mask 0 (no edges active) and mask 16 (no corners active) */
3190 if ((m < ISO_NUM_EDGE_MASKS && m == 0) ||
3191 (m >= ISO_NUM_EDGE_MASKS && m == ISO_NUM_EDGE_MASKS)) {
3192 tiles[t][m] = NULL;
3193 continue;
3194 }
3195
3196 tiles[t][m] = al_create_bitmap(tile_w, tile_h);
3197 al_set_target_bitmap(tiles[t][m]);
3198 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3199
3200 /* Draw the terrain tile first */
3201 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
3202 al_draw_bitmap(tile_bitmaps[t], 0, 0, 0);
3203
3204 /* Multiply by the alpha mask: keeps terrain color
3205 * but applies the mask's alpha for the transition gradient */
3206 al_set_blender(ALLEGRO_ADD, ALLEGRO_DEST_COLOR, ALLEGRO_ZERO);
3207 al_draw_bitmap(masks[m], 0, 0, 0);
3208 }
3209 }
3210
3211 al_restore_state(&state);
3212 n_log(LOG_INFO, "Pre-composited transition tiles for %d terrains x %d masks", num_terrains, ISO_NUM_MASKS);
3213}
3214
3215#endif /* HAVE_ALLEGRO - iso_map_draw */
3216
3217#ifdef N_ASTAR_H
3227ASTAR_GRID* iso_map_to_astar_grid(const ISO_MAP* map, int max_height_diff, int start_x, int start_y) {
3228 __n_assert(map, return NULL);
3229
3230 ASTAR_GRID* grid = n_astar_grid_new(map->width, map->height, 1);
3231 if (!grid) return NULL;
3232
3233 int total = map->width * map->height;
3234
3235 /* First pass: all cells unwalkable, set costs */
3236 for (int y = 0; y < map->height; y++) {
3237 for (int x = 0; x < map->width; x++) {
3238 n_astar_grid_set_walkable(grid, x, y, 0, 0);
3239 int h = map->heightmap[y * map->width + x];
3240 int cost = ASTAR_COST_CARDINAL + h * 100;
3241 n_astar_grid_set_cost(grid, x, y, 0, cost);
3242 }
3243 }
3244
3245 /* Flood-fill from (start_x, start_y) to mark reachable cells as walkable.
3246 * This ensures height constraints are checked per-edge (between consecutive
3247 * cells) rather than per-cell, so tiles on a plateau edge remain walkable
3248 * when approached from the same elevation. */
3249 if (start_x < 0 || start_x >= map->width || start_y < 0 || start_y >= map->height)
3250 return grid;
3251
3252 int start_ab = map->ability[start_y * map->width + start_x];
3253 if (start_ab != WALK && start_ab != SWIM)
3254 return grid;
3255
3256 int* queue = (int*)malloc((size_t)total * 2 * sizeof(int));
3257 if (!queue) {
3258 n_astar_grid_free(grid);
3259 return NULL;
3260 }
3261
3262 uint8_t* visited = (uint8_t*)calloc((size_t)total, sizeof(uint8_t));
3263 if (!visited) {
3264 free(queue);
3265 n_astar_grid_free(grid);
3266 return NULL;
3267 }
3268
3269 int qfront = 0, qback = 0;
3270
3271 /* seed the start cell */
3272 visited[start_y * map->width + start_x] = 1;
3273 n_astar_grid_set_walkable(grid, start_x, start_y, 0, 1);
3274 queue[qback++] = start_x;
3275 queue[qback++] = start_y;
3276
3277 /* 8-directional expansion (matches ASTAR_ALLOW_DIAGONAL) */
3278 const int dirs[][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}};
3279
3280 while (qfront < qback) {
3281 int cx = queue[qfront++];
3282 int cy = queue[qfront++];
3283 int ch = map->heightmap[cy * map->width + cx];
3284
3285 for (int d = 0; d < 8; d++) {
3286 int nx = cx + dirs[d][0];
3287 int ny = cy + dirs[d][1];
3288 if (nx < 0 || nx >= map->width || ny < 0 || ny >= map->height) continue;
3289 if (visited[ny * map->width + nx]) continue;
3290
3291 int nab = map->ability[ny * map->width + nx];
3292 if (nab != WALK && nab != SWIM) continue;
3293
3294 int nh = map->heightmap[ny * map->width + nx];
3295 if (max_height_diff >= 0 && abs(ch - nh) > max_height_diff) continue;
3296
3297 visited[ny * map->width + nx] = 1;
3298 n_astar_grid_set_walkable(grid, nx, ny, 0, 1);
3299 queue[qback++] = nx;
3300 queue[qback++] = ny;
3301 }
3302 }
3303
3304 free(queue);
3305 free(visited);
3306
3307 return grid;
3308}
3309#endif /* N_ASTAR_H */
3310
3311#endif /* #ifndef NOISOENGINE */
static int player_mode
Definition ex_gui.c:60
static ALLEGRO_BITMAP * transition_tiles[8][(16+16)]
static int screen_w
static ALLEGRO_BITMAP * tile_bitmaps[8]
static int screen_h
static int mode
#define M_PI
char * key
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:89
#define LOG_ERR
error conditions
Definition n_log.h:76
#define LOG_INFO
informational
Definition n_log.h:82
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 ALLEGRO_COLOR _height_tint(int seg_top, int max_height, float intensity)
Compute a brightness tint color for a segment at a given height.
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...