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 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <https://www.gnu.org/licenses/>.
17 */
18
28#include <math.h>
29#include <stdio.h>
30
31/* Include n_astar.h BEFORE n_iso_engine.h so the iso_map_to_astar_grid
32 * function is enabled via the N_ASTAR_H guard */
33#include "nilorea/n_astar.h"
35
36#ifndef NOISOENGINE
37
38/*===========================================================================
39 * Legacy API (requires Allegro 5)
40 * Only compiled when HAVE_ALLEGRO is defined.
41 *=========================================================================*/
42#ifdef HAVE_ALLEGRO
43
59int create_empty_map(MAP** map, const char* name, int XSIZE, int YSIZE, int TILEW, int TILEH, int nbmaxobjects, int nbmaxgroup, int nbmaxanims, int nbtiles, int nbanims) {
60 int x, y;
61
62 (void)nbmaxobjects;
63 (void)nbmaxgroup;
64 (void)nbmaxanims;
65 (void)nbtiles;
66 (void)nbanims;
67
68 /* allocating map */
69 Malloc((*map), MAP, 1);
70
71 if (!(*map))
72 return FALSE;
73
74 /* allocating grid */
75 Malloc((*map)->grid, CELL, (size_t)XSIZE * (size_t)YSIZE);
76 if (!(*map)->grid) {
77 Free((*map));
78 return FALSE;
79 }
80
81 /* allocating name */
82 Malloc((*map)->name, char, strlen(name) + 1);
83 if (!(*map)->name) {
84 Free((*map)->grid);
85 Free((*map));
86 return FALSE;
87 }
88
89 strcpy((*map)->name, name);
90
91 (*map)->XSIZE = XSIZE;
92 (*map)->YSIZE = YSIZE;
93
94 for (x = 0; x < XSIZE; x++) {
95 for (y = 0; y < YSIZE; y++) {
96 set_value((*map), N_TILE, x, y, FALSE);
97 set_value((*map), N_ABILITY, x, y, FALSE);
98 set_value((*map), N_MUSIC, x, y, FALSE);
99 set_value((*map), N_OBJECT, x, y, FALSE);
100 }
101 }
102
103 (*map)->TILEW = TILEW;
104 (*map)->TILEH = TILEH;
105 (*map)->bgcolor = al_map_rgba(0, 0, 0, 255);
106 (*map)->wirecolor = al_map_rgba(255, 255, 255, 255);
107
108 /* Drawing the mousemap and colortile */
109 ALLEGRO_BITMAP* prev_target = al_get_target_bitmap();
110
111 (*map)->mousemap = al_create_bitmap((*map)->TILEW, (*map)->TILEH);
112 (*map)->colortile = al_create_bitmap((*map)->TILEW, (*map)->TILEH);
113
114 float tw = (float)TILEW;
115 float th = (float)TILEH;
116 float tw2 = tw / 2.0f;
117 float th2 = th / 2.0f;
118
119 /* drawing mousemap: white diamond center, colored corner triangles */
120 al_set_target_bitmap((*map)->mousemap);
121 al_clear_to_color(al_map_rgb(255, 255, 255));
122
123 /* top-left corner: red */
124 al_draw_filled_triangle(0, 0, tw2, 0, 0, th2, al_map_rgb(255, 0, 0));
125 /* top-right corner: yellow */
126 al_draw_filled_triangle(tw2, 0, tw, 0, tw, th2, al_map_rgb(255, 255, 0));
127 /* bottom-left corner: green */
128 al_draw_filled_triangle(0, th2, 0, th, tw2, th, al_map_rgb(0, 255, 0));
129 /* bottom-right corner: blue */
130 al_draw_filled_triangle(tw, th2, tw2, th, tw, th, al_map_rgb(0, 0, 255));
131
132 /* drawing colortile: transparent background, bgcolor diamond */
133 al_set_target_bitmap((*map)->colortile);
134 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
135
136 /* draw diamond as two triangles filled with bgcolor */
137 al_draw_filled_triangle(tw2, 0, 0, th2, tw, th2, (*map)->bgcolor);
138 al_draw_filled_triangle(0, th2, tw2, th, tw, th2, (*map)->bgcolor);
139
140 /* restore previous target */
141 if (prev_target)
142 al_set_target_bitmap(prev_target);
143
144 (*map)->ptanchorX = (*map)->X = 0;
145 (*map)->ptanchorY = (*map)->Y = 0;
146
147 return TRUE;
148} /* create_empty_map( ... ) */
149
159int set_value(MAP* map, int type, int x, int y, int value) {
160 if (!map || x < 0 || y < 0 || x >= map->XSIZE || y >= map->YSIZE)
161 return FALSE;
162
163 if (type == N_TILE) {
164 map->grid[x + y * map->XSIZE].tilenumber = value;
165
166 return TRUE;
167 }
168
169 if (type == N_ABILITY) {
170 map->grid[x + y * map->XSIZE].ability = value;
171
172 return TRUE;
173 }
174
175 if (type == N_MUSIC) {
176 map->grid[x + y * map->XSIZE].music = value;
177
178 return TRUE;
179 }
180
181 if (type == N_OBJECT) {
182 map->grid[x + y * map->XSIZE].objectnumber = value;
183
184 return TRUE;
185 }
186
187 return FALSE;
188} /* set_value( ... ) */
189
198int get_value(MAP* map, int type, int x, int y) {
199 if (!map || x < 0 || y < 0 || x >= map->XSIZE || y >= map->YSIZE)
200 return FALSE;
201
202 if (type == N_TILE)
203 return map->grid[x + y * map->XSIZE].tilenumber;
204
205 if (type == N_OBJECT)
206 return map->grid[x + y * map->XSIZE].objectnumber;
207
208 if (type == N_MUSIC)
209 return map->grid[x + y * map->XSIZE].music;
210
211 if (type == N_ABILITY)
212 return map->grid[x + y * map->XSIZE].ability;
213
214 return FALSE;
215
216} /* get_value( ... ) */
217
227int ScreenToMap(int mx, int my, int* Tilex, int* Tiley, ALLEGRO_BITMAP* mousemap) {
228 /* SCREEN TO MAP VARIABLES */
229 int RegionX, RegionY,
230 RegionDX = 0, RegionDY = 0,
231 MouseMapX, MouseMapY,
232 MouseMapWidth, MouseMapHeight;
233
234 MouseMapWidth = al_get_bitmap_width(mousemap);
235 MouseMapHeight = al_get_bitmap_height(mousemap);
236
237 /* First step find out where we are on the mousemap */
238 RegionX = (mx / MouseMapWidth);
239 RegionY = (my / MouseMapHeight) << 1; /* The multiplying by two is very important */
240
241 /* Second Step: Find out WHERE in the mousemap our mouse is, by finding MouseMapX and MouseMapY. */
242
243 MouseMapX = mx % MouseMapWidth;
244 MouseMapY = my % MouseMapHeight;
245
246 /* third step: Find the color in the mouse map */
247 ALLEGRO_COLOR c = al_get_pixel(mousemap, MouseMapX, MouseMapY);
248 unsigned char r, g, b;
249 al_unmap_rgb(c, &r, &g, &b);
250
251 if (r == 255 && g == 0 && b == 0) {
252 /* red: top-left */
253 RegionDX = -1;
254 RegionDY = -1;
255 }
256
257 if (r == 255 && g == 255 && b == 0) {
258 /* yellow: top-right */
259 RegionDX = 0;
260 RegionDY = -1;
261 }
262
263 if (r == 255 && g == 255 && b == 255) {
264 /* white: center diamond */
265 RegionDX = 0;
266 RegionDY = 0;
267 }
268
269 if (r == 0 && g == 255 && b == 0) {
270 /* green: bottom-left */
271 RegionDX = -1;
272 RegionDY = 1;
273 }
274
275 if (r == 0 && g == 0 && b == 255) {
276 /* blue: bottom-right */
277 RegionDX = 0;
278 RegionDY = 1;
279 }
280
281 *Tilex = (RegionDX + RegionX);
282 *Tiley = (RegionDY + RegionY);
283
284 return TRUE;
285} /* ScreenToMap( ... ) */
286
294int camera_to_scr(MAP** map, int x, int y) {
295 if (x < 0)
296 x = 0;
297
298 if (y < 0)
299 y = 0;
300
301 (*map)->X = x % (*map)->TILEW;
302 (*map)->Y = y % (*map)->TILEH;
303 (*map)->ptanchorY = y / ((*map)->TILEH >> 1);
304 (*map)->ptanchorY = (*map)->ptanchorY >> 1;
305 (*map)->ptanchorY = (*map)->ptanchorY << 1;
306 (*map)->ptanchorX = (x + ((*map)->ptanchorY & 1) * ((*map)->TILEW >> 1)) / (*map)->TILEW;
307
308 return TRUE;
309} /* camera_to_scr( ... )*/
310
320int camera_to_map(MAP** map, int tx, int ty, int x, int y) {
321 (*map)->ptanchorX = tx;
322 (*map)->ptanchorY = ty;
323 (*map)->X = x;
324 (*map)->Y = y;
325
326 return TRUE;
327} /* camera_to_map(...) */
328
338int draw_map(MAP* map, ALLEGRO_BITMAP* bmp, int destx, int desty, int mode) {
339 int a, b, /* iterators */
340 mvx, mvy, /* precomputed offset */
341 tw, th, /* number of tile for one screen */
342 TW2, TH2; /* size of TILE_W /2 and TILE_H/2 */
343
344 ALLEGRO_BITMAP* prev_target = al_get_target_bitmap();
345 al_set_target_bitmap(bmp);
346
347 int bmp_w = al_get_bitmap_width(bmp);
348 int bmp_h = al_get_bitmap_height(bmp);
349
350 /* computing tw&th */
351 tw = 1 + ((bmp_w - destx) / map->TILEW);
352 th = 3 + ((bmp_h - desty) / (map->TILEH >> 1));
353
354 TW2 = map->TILEW >> 1;
355 TH2 = map->TILEH >> 1;
356
357 mvx = destx - TW2 - map->X;
358 mvy = desty - TH2 - map->Y;
359
360 for (a = 0; a <= tw; a++) {
361 for (b = 0; b <= th; b++) {
362 int x = (a * map->TILEW + ((b & 1) * TW2)) - (map->TILEW >> 1) + mvx;
363 int y = (b * TH2) + mvy; // cppcheck-suppress variableScope -- used in multiple sub-blocks
364
365 if ((a + map->ptanchorX) < map->XSIZE && (b + map->ptanchorY) < map->YSIZE) {
366 if (map->grid[a + map->ptanchorX + (b + map->ptanchorY) * map->XSIZE].tilenumber == FALSE) {
367 al_draw_bitmap(map->colortile, (float)x, (float)y, 0);
368 }
369
370 else {
371 /* placeholder: add sprite blit */
372 float fx = (float)x;
373 float fy = (float)y;
374 float ftw = (float)map->TILEW;
375 float fth = (float)map->TILEH;
376
377 al_draw_filled_triangle(fx, fy + fth / 2.0f,
378 fx + ftw, fy + fth / 2.0f,
379 fx + ftw / 2.0f, fy,
380 al_map_rgb(255, 255, 0));
381 al_draw_filled_triangle(fx, fy + fth / 2.0f,
382 fx + ftw, fy + fth / 2.0f,
383 fx + ftw / 2.0f, fy + fth,
384 al_map_rgb(255, 0, 255));
385
386 } /* if( map -> grid... ) ) */
387
388 if (mode == 1) {
389 float fx = (float)x;
390 float fy = (float)y;
391 float ftw = (float)map->TILEW;
392 float fth = (float)map->TILEH;
393
394 al_draw_line(fx + ftw / 2.0f - 2, fy,
395 fx, fy + fth / 2.0f - 1,
396 map->wirecolor, 0);
397 al_draw_line(fx + ftw / 2.0f - 2, fy + fth - 1,
398 fx, fy + fth / 2.0f,
399 map->wirecolor, 0);
400 al_draw_line(fx + ftw / 2.0f + 1, fy + fth - 1,
401 fx + ftw - 1, fy + fth / 2.0f,
402 map->wirecolor, 0);
403 al_draw_line(fx + ftw / 2.0f + 1, fy,
404 fx + ftw - 1, fy + fth / 2.0f - 1,
405 map->wirecolor, 0);
406 }
407
408 /* TODO: Object-library TTL handling is not implemented here.
409 * Intended behavior:
410 * - Check if object is in the library; if yes, refresh its TTL.
411 * - If not, add it with the default TTL.
412 */
413
414 } /* if( (a + ...) ) */
415
416 } /* for( a... ) */
417 } /* for( b... ) */
418
419 /* TODO: sort the object list by X and by Y and blit all the sprites of the active list */
420
421 /* restore previous target */
422 if (prev_target)
423 al_set_target_bitmap(prev_target);
424
425 return TRUE;
426
427} /* draw_map( ... ) */
428
435int load_map(MAP** map, char* filename) {
436 ALLEGRO_FILE* loaded;
437
438 char temp_name[1024];
439 int temp_XSIZE, temp_YSIZE, temp_TILEW, temp_TILEH;
440 int temp_ptanchorX, temp_ptanchorY, temp_X, temp_Y;
441 uint32_t temp_bgcolor, temp_wirecolor;
442
443 int it = 0;
444
445 loaded = al_fopen(filename, "rb");
446 if (!loaded) {
447 n_log(LOG_ERR, "could not open %s for reading", filename);
448 return FALSE;
449 }
450
451 if (map && *map)
452 free_map(map);
453
454 /* reading name */
455 it = al_fread32le(loaded);
456
457 if (it > 1023) it = 1023;
458 al_fread(loaded, temp_name, (size_t)it);
459 temp_name[it] = '\0';
460
461 /* reading states */
462 temp_XSIZE = al_fread32le(loaded);
463 temp_YSIZE = al_fread32le(loaded);
464 temp_TILEW = al_fread32le(loaded);
465 temp_TILEH = al_fread32le(loaded);
466 temp_ptanchorX = al_fread32le(loaded);
467 temp_ptanchorY = al_fread32le(loaded);
468 temp_X = al_fread32le(loaded);
469 temp_Y = al_fread32le(loaded);
470 temp_bgcolor = (uint32_t)al_fread32le(loaded);
471 temp_wirecolor = (uint32_t)al_fread32le(loaded);
472
473 if (create_empty_map(map, temp_name,
474 temp_XSIZE, temp_YSIZE,
475 temp_TILEW, temp_TILEH,
476 2000, 2000, 2000, 2000, 2000) != TRUE) {
477 al_fclose(loaded);
478 return FALSE;
479 }
480 (*map)->XSIZE = temp_XSIZE;
481 (*map)->YSIZE = temp_YSIZE;
482 (*map)->TILEW = temp_TILEW;
483 (*map)->TILEH = temp_TILEH;
484 (*map)->ptanchorX = temp_ptanchorX;
485 (*map)->ptanchorY = temp_ptanchorY;
486 (*map)->X = temp_X;
487 (*map)->Y = temp_Y;
488 (*map)->bgcolor = al_map_rgba((unsigned char)((temp_bgcolor >> 24) & 0xFF),
489 (unsigned char)((temp_bgcolor >> 16) & 0xFF),
490 (unsigned char)((temp_bgcolor >> 8) & 0xFF),
491 (unsigned char)(temp_bgcolor & 0xFF));
492 (*map)->wirecolor = al_map_rgba((unsigned char)((temp_wirecolor >> 24) & 0xFF),
493 (unsigned char)((temp_wirecolor >> 16) & 0xFF),
494 (unsigned char)((temp_wirecolor >> 8) & 0xFF),
495 (unsigned char)(temp_wirecolor & 0xFF));
496
497 /* loading the grid */
498 for (it = 0; it < (*map)->XSIZE; it++) {
499 for (int it1 = 0; it1 < (*map)->YSIZE; it1++) {
500 (*map)->grid[it + it1 * (*map)->XSIZE].tilenumber = al_fread32le(loaded);
501 (*map)->grid[it + it1 * (*map)->XSIZE].ability = al_fread32le(loaded);
502 (*map)->grid[it + it1 * (*map)->XSIZE].objectnumber = al_fread32le(loaded);
503 (*map)->grid[it + it1 * (*map)->XSIZE].music = al_fread32le(loaded);
504 }
505 }
506
507 /*load_animlib( loaded , & (*map ) -> libtiles );*/
508 /*load_animlib( loaded , & (*map ) -> libanims );*/
509
510 al_fclose(loaded);
511
512 return TRUE;
513} /* load_map( ... ) */
514
521int save_map(MAP* map, char* filename) {
522 ALLEGRO_FILE* saved;
523
524 int it = 0;
525
526 saved = al_fopen(filename, "wb");
527 if (!saved) {
528 n_log(LOG_ERR, "could not open %s for writing", filename);
529 return FALSE;
530 }
531
532 /* writing name */
533 int name_len = (int)(strlen(map->name) + 1);
534 al_fwrite32le(saved, name_len);
535 al_fwrite(saved, map->name, (size_t)name_len);
536
537 /* writing states */
538 al_fwrite32le(saved, map->XSIZE);
539 al_fwrite32le(saved, map->YSIZE);
540 al_fwrite32le(saved, map->TILEW);
541 al_fwrite32le(saved, map->TILEH);
542 al_fwrite32le(saved, map->ptanchorX);
543 al_fwrite32le(saved, map->ptanchorY);
544 al_fwrite32le(saved, map->X);
545 al_fwrite32le(saved, map->Y);
546
547 /* pack ALLEGRO_COLOR into 32-bit RGBA */
548 unsigned char r, g, b, a;
549 al_unmap_rgba(map->bgcolor, &r, &g, &b, &a);
550 al_fwrite32le(saved, (int32_t)(((uint32_t)r << 24) | ((uint32_t)g << 16) | ((uint32_t)b << 8) | (uint32_t)a));
551 al_unmap_rgba(map->wirecolor, &r, &g, &b, &a);
552 al_fwrite32le(saved, (int32_t)(((uint32_t)r << 24) | ((uint32_t)g << 16) | ((uint32_t)b << 8) | (uint32_t)a));
553
554 /* saving the grid */
555 for (it = 0; it < map->XSIZE; it++) {
556 for (int it1 = 0; it1 < map->YSIZE; it1++) {
557 al_fwrite32le(saved, map->grid[it + it1 * map->XSIZE].tilenumber);
558 al_fwrite32le(saved, map->grid[it + it1 * map->XSIZE].ability);
559 al_fwrite32le(saved, map->grid[it + it1 * map->XSIZE].objectnumber);
560 al_fwrite32le(saved, map->grid[it + it1 * map->XSIZE].music);
561 }
562 }
563
564 /*save_animlib( saved , map -> libtiles );*/
565 /*save_animlib( saved , map -> libanims );*/
566
567 al_fclose(saved);
568
569 return TRUE;
570} /* save_map( ... ) */
571
577int free_map(MAP** map) {
578 if (!(*map))
579 return TRUE;
580 Free((*map)->grid);
581 Free((*map)->name);
582 if ((*map)->colortile)
583 al_destroy_bitmap((*map)->colortile);
584 if ((*map)->mousemap)
585 al_destroy_bitmap((*map)->mousemap);
586
587 Free((*map));
588
589 return TRUE;
590} /* free_map( ... ) */
591
592#endif /* HAVE_ALLEGRO - end of legacy API */
593
594/*===========================================================================
595 * New height-aware ISO_MAP API (Articles 747/748/934/1269/2026)
596 * This section does NOT require Allegro and compiles everywhere.
597 *=========================================================================*/
598
607ISO_MAP* iso_map_new(int width, int height, int num_terrains, int max_height) {
608 __n_assert(width > 0, return NULL);
609 __n_assert(height > 0, return NULL);
610
611 ISO_MAP* map = NULL;
612 Malloc(map, ISO_MAP, 1);
613 __n_assert(map, return NULL);
614
615 map->width = width;
616 map->height = height;
617 map->num_terrains = num_terrains;
618 map->max_height = max_height;
619
620 size_t total = (size_t)width * (size_t)height;
621
622 Malloc(map->terrain, int, total);
623 if (!map->terrain) {
624 Free(map);
625 return NULL;
626 }
627
628 Malloc(map->heightmap, int, total);
629 if (!map->heightmap) {
630 Free(map->terrain);
631 Free(map);
632 return NULL;
633 }
634
635 Malloc(map->ability, int, total);
636 if (!map->ability) {
637 Free(map->heightmap);
638 Free(map->terrain);
639 Free(map);
640 return NULL;
641 }
642
643 memset(map->terrain, 0, total * sizeof(int));
644 memset(map->heightmap, 0, total * sizeof(int));
645 for (size_t i = 0; i < total; i++) {
646 map->ability[i] = WALK;
647 }
648
649 /* default classic 2:1 projection for 64px wide tiles */
651
652 map->segments = NULL; /* allocated on demand by iso_map_set_segments() */
653
654 /* Overlay layer: allocated and zeroed, 0 = no overlay */
655 Malloc(map->overlay, int, total);
656 if (map->overlay) memset(map->overlay, 0, total * sizeof(int));
657
658 /* default rendering flags */
659 map->smooth_height = 0;
660 map->smooth_slope_max = 1;
661 map->show_grid = 0;
662 map->hover_mx = -1;
663 map->hover_my = -1;
664 map->height_tint_intensity = 0.0f; /* disabled by default; game sets this */
665
666 /* Ambient color defaults: full daylight (no tinting) */
667 map->ambient_r = 1.0f;
668 map->ambient_g = 1.0f;
669 map->ambient_b = 1.0f;
670 map->dynamic_light_map = NULL; /* caller-owned, set before draw */
671
672 return map;
673} /* iso_map_new() */
674
679void iso_map_free(ISO_MAP** map_ptr) {
680 __n_assert(map_ptr, return);
681 __n_assert(*map_ptr, return);
682 ISO_MAP* map = *map_ptr;
683 Free(map->terrain);
684 Free(map->heightmap);
685 Free(map->ability);
686 Free(map->segments);
687 Free(map->overlay);
688 Free(*map_ptr);
689} /* iso_map_free() */
690
696static float _iso_preset_angle(int preset) {
697 switch (preset) {
699 return 30.0f;
701 return 18.43f;
703 return 45.0f;
704 case ISO_PROJ_CLASSIC:
705 default:
706 return 26.565f;
707 }
708}
709
716void iso_map_set_projection(ISO_MAP* map, int preset, float tile_width) {
717 __n_assert(map, return);
718 float hw = tile_width / 2.0f;
719 float angle = _iso_preset_angle(preset);
720 float hh = hw * tanf(angle * (float)M_PI / 180.0f);
721
722 map->proj.half_w = hw;
723 map->proj.half_h = hh;
724 map->proj.angle_deg = angle;
725 map->proj.target_angle = angle;
726 map->proj.lerp_speed = 3.0f;
727 map->proj.tile_lift = hh;
728} /* iso_map_set_projection() */
729
737 __n_assert(map, return);
738 map->proj.target_angle = _iso_preset_angle(preset);
739} /* iso_map_set_projection_target() */
740
747void iso_map_lerp_projection(ISO_MAP* map, float dt) {
748 __n_assert(map, return);
749 float diff = map->proj.target_angle - map->proj.angle_deg;
750 if (fabsf(diff) < 0.01f) {
751 map->proj.angle_deg = map->proj.target_angle;
752 } else {
753 map->proj.angle_deg += diff * map->proj.lerp_speed * dt;
754 }
755 float rad = map->proj.angle_deg * (float)M_PI / 180.0f;
756 map->proj.half_h = map->proj.half_w * sinf(rad) / cosf(rad);
757 if (map->proj.half_h < 8.0f) map->proj.half_h = 8.0f;
758 if (map->proj.half_h > map->proj.half_w * 2.0f) map->proj.half_h = map->proj.half_w * 2.0f;
759} /* iso_map_lerp_projection() */
760
766const char* iso_projection_name(int preset) {
767 switch (preset) {
768 case ISO_PROJ_CLASSIC:
769 return "Classic 2:1";
771 return "True Isometric";
773 return "Staggered";
775 return "Military";
776 default:
777 return "Unknown";
778 }
779} /* iso_projection_name() */
780
788void iso_map_set_projection_custom(ISO_MAP* map, float half_w, float half_h, float tile_lift) {
789 __n_assert(map, return);
790 map->proj.half_w = half_w;
791 map->proj.half_h = half_h;
792 map->proj.tile_lift = tile_lift;
793 map->proj.angle_deg = atanf(half_h / half_w) * 180.0f / (float)M_PI;
794 map->proj.target_angle = map->proj.angle_deg;
795} /* iso_map_set_projection_custom() */
796
804int iso_map_get_terrain(const ISO_MAP* map, int mx, int my) {
805 __n_assert(map, return 0);
806 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
807 return map->terrain[my * map->width + mx];
808} /* iso_map_get_terrain() */
809
817void iso_map_set_terrain(ISO_MAP* map, int mx, int my, int terrain) {
818 __n_assert(map, return);
819 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
820 map->terrain[my * map->width + mx] = terrain;
821} /* iso_map_set_terrain() */
822
830int iso_map_get_height(const ISO_MAP* map, int mx, int my) {
831 __n_assert(map, return 0);
832 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
833 return map->heightmap[my * map->width + mx];
834} /* iso_map_get_height() */
835
843void iso_map_set_height(ISO_MAP* map, int mx, int my, int h) {
844 __n_assert(map, return);
845 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
846 if (h < 0) h = 0;
847 if (h > map->max_height) h = map->max_height;
848 map->heightmap[my * map->width + mx] = h;
849} /* iso_map_set_height() */
850
858int iso_map_get_ability(const ISO_MAP* map, int mx, int my) {
859 __n_assert(map, return 0);
860 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
861 return map->ability[my * map->width + mx];
862} /* iso_map_get_ability() */
863
871void iso_map_set_ability(ISO_MAP* map, int mx, int my, int ab) {
872 __n_assert(map, return);
873 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return;
874 map->ability[my * map->width + mx] = ab;
875} /* iso_map_set_ability() */
876
889void iso_map_to_screen(const ISO_MAP* map, int mx, int my, int h, float* screen_x, float* screen_y) {
890 __n_assert(map, return);
891 __n_assert(screen_x, return);
892 __n_assert(screen_y, return);
893 *screen_x = ((float)mx - (float)my) * map->proj.half_w + map->proj.half_w;
894 *screen_y = ((float)mx + (float)my) * map->proj.half_h - (float)h * map->proj.tile_lift;
895} /* iso_map_to_screen() */
896
907void iso_screen_to_map(const ISO_MAP* map, float screen_x, float screen_y, int* mx, int* my) {
908 __n_assert(map, return);
909 __n_assert(mx, return);
910 __n_assert(my, return);
911 float tw = 2.0f * map->proj.half_w;
912 float th = 2.0f * map->proj.half_h;
913 *mx = (int)floorf(screen_x / tw + screen_y / th - 1.0f);
914 *my = (int)floorf(screen_y / th - screen_x / tw + 1.0f);
915} /* iso_screen_to_map() */
916
930void iso_map_to_screen_f(const ISO_MAP* map, float fmx, float fmy, float h, float* screen_x, float* screen_y) {
931 __n_assert(map, return);
932 __n_assert(screen_x, return);
933 __n_assert(screen_y, return);
934 *screen_x = (fmx - fmy) * map->proj.half_w + 2.0f * map->proj.half_w;
935 *screen_y = (fmx + fmy) * map->proj.half_h - h * map->proj.tile_lift;
936} /* iso_map_to_screen_f() */
937
957void 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) {
958 __n_assert(map, return);
959 __n_assert(sx, return);
960 __n_assert(sy, return);
961 float hw = map->proj.half_w;
962 float hh = map->proj.half_h;
963 float tl = map->proj.tile_lift;
964 float wx = (float)(cx - cy) * hw + 2.0f * hw;
965 float wy = (float)(cx + cy) * hh - fh * tl;
966 *sx = wx * zoom + cam_px;
967 *sy = wy * zoom + cam_py;
968} /* iso_corner_to_screen() */
969
979int iso_is_in_diamond(int px, int py, int tile_w, int tile_h) {
980 float cx = (float)tile_w / 2.0f;
981 float cy = (float)tile_h / 2.0f;
982 float dx = fabsf((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
983 float dy = fabsf((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
984 return (dx + dy) <= 1.0f;
985} /* iso_is_in_diamond() */
986
997float iso_diamond_dist(int px, int py, int tile_w, int tile_h) {
998 float cx = (float)tile_w / 2.0f;
999 float cy = (float)tile_h / 2.0f;
1000 float dx = fabsf((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
1001 float dy = fabsf((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
1002 float d = dx + dy;
1003 if (d > 1.0f) return 0.0f;
1004 return 1.0f - d;
1005} /* iso_diamond_dist() */
1006
1020void 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) {
1021 __n_assert(map, return);
1022 __n_assert(mx, return);
1023 __n_assert(my, return);
1024
1025 float half_w = map->proj.half_w;
1026 float half_h = map->proj.half_h;
1027 float tile_lift = map->proj.tile_lift;
1028 float tw = half_w * 2.0f;
1029 float th = half_h * 2.0f;
1030
1031 for (int h = map->max_height; h >= 0; h--) {
1032 float adj_sy = screen_y + (float)h * tile_lift;
1033 float fmx = screen_x / tw + adj_sy / th - 1.0f;
1034 float fmy = adj_sy / th - screen_x / tw + 1.0f;
1035 int tmx = (int)floorf(fmx);
1036 int tmy = (int)floorf(fmy);
1037
1038 if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height)
1039 continue;
1040
1041 if (iso_map_get_height(map, tmx, tmy) == h) {
1042 float tile_sx, tile_sy;
1043 iso_map_to_screen(map, tmx, tmy, h, &tile_sx, &tile_sy);
1044 float norm_x = (screen_x - tile_sx) / tw * (float)tile_w;
1045 float norm_y = (screen_y - tile_sy) / th * (float)tile_h;
1046 int lpx = (int)norm_x;
1047 int lpy = (int)norm_y;
1048 if (lpx >= 0 && lpx < tile_w && lpy >= 0 && lpy < tile_h &&
1049 iso_is_in_diamond(lpx, lpy, tile_w, tile_h)) {
1050 *mx = tmx;
1051 *my = tmy;
1052 return;
1053 }
1054 }
1055 }
1056
1057 /* Fallback: flat projection (height 0) */
1058 *mx = (int)floorf(screen_x / tw + screen_y / th - 1.0f);
1059 *my = (int)floorf(screen_y / th - screen_x / tw + 1.0f);
1060} /* iso_screen_to_map_height() */
1061
1076void 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) {
1077 __n_assert(map, return);
1078 __n_assert(mx, return);
1079 __n_assert(my, return);
1080
1081 float half_w = map->proj.half_w;
1082 float half_h = map->proj.half_h;
1083 float tile_lift = map->proj.tile_lift;
1084 float tw = half_w * 2.0f;
1085 float th = half_h * 2.0f;
1086
1087 for (int h = map->max_height; h >= 0; h--) {
1088 float adj_sy = screen_y + (float)h * tile_lift;
1089 float fmx = screen_x / tw + adj_sy / th - 1.0f;
1090 float fmy = adj_sy / th - screen_x / tw + 1.0f;
1091 int tmx = (int)floorf(fmx);
1092 int tmy = (int)floorf(fmy);
1093
1094 if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height)
1095 continue;
1096
1097 if (iso_map_get_height(map, tmx, tmy) == h) {
1098 float tile_sx, tile_sy;
1099 iso_map_to_screen(map, tmx, tmy, h, &tile_sx, &tile_sy);
1100 float norm_x = (screen_x - tile_sx) / tw * (float)tile_w;
1101 float norm_y = (screen_y - tile_sy) / th * (float)tile_h;
1102 int lpx = (int)norm_x;
1103 int lpy = (int)norm_y;
1104 if (lpx >= 0 && lpx < tile_w && lpy >= 0 && lpy < tile_h &&
1105 iso_is_in_diamond(lpx, lpy, tile_w, tile_h)) {
1106 *mx = tmx;
1107 *my = tmy;
1108 if (out_fx || out_fy) {
1109 /* Refine fractional coordinates using interpolated height
1110 * so that iso_map_to_screen_f(fx, fy, interp_h) returns
1111 * the original screen position. Without this, sub-tile
1112 * positions are off when the bilinear-interpolated height
1113 * differs from the tile's integer height (e.g. at edges
1114 * between tiles of different heights).
1115 *
1116 * Only refine in SMOOTH mode. In CUT mode the rendered
1117 * tile sits at the flat integer height, so the initial
1118 * fmx/fmy (computed at that height) are already correct.
1119 * Refining with bilinear-interpolated heights in CUT mode
1120 * pulls the position toward tile corners and causes
1121 * click-to-move to land at the wrong spot. */
1122 float refined_fx = fmx;
1123 float refined_fy = fmy;
1124 if (map->smooth_height) {
1125 /* Upper bounds: allow the full extent of the last tile.
1126 * Tile tmx occupies [tmx, tmx+1) in map coords, and the
1127 * last valid position must stay < map->width so that
1128 * floor() never yields an out-of-bounds tile index. */
1129 float map_max_fx = (float)map->width - 1e-4f;
1130 float map_max_fy = (float)map->height - 1e-4f;
1131 /* Tile-local bounds to prevent divergence: clamp each
1132 * refinement step to the detected tile so that large
1133 * height differences with neighbours cannot pull the
1134 * position into an adjacent tile. */
1135 float tile_min_fx = (float)tmx;
1136 float tile_min_fy = (float)tmy;
1137 float tile_max_fx = (float)(tmx + 1);
1138 float tile_max_fy = (float)(tmy + 1);
1139 if (tile_max_fx > map_max_fx) tile_max_fx = map_max_fx;
1140 if (tile_max_fy > map_max_fy) tile_max_fy = map_max_fy;
1141 for (int iter = 0; iter < 3; iter++) {
1142 float interp_h = iso_map_interpolate_height(map, refined_fx, refined_fy);
1143 float new_adj_sy = screen_y + interp_h * tile_lift;
1144 refined_fx = screen_x / tw + new_adj_sy / th - 1.0f;
1145 refined_fy = new_adj_sy / th - screen_x / tw + 1.0f;
1146 /* Keep within detected tile */
1147 if (refined_fx < tile_min_fx) refined_fx = tile_min_fx;
1148 if (refined_fy < tile_min_fy) refined_fy = tile_min_fy;
1149 if (refined_fx > tile_max_fx) refined_fx = tile_max_fx;
1150 if (refined_fy > tile_max_fy) refined_fy = tile_max_fy;
1151 }
1152 }
1153 /* Clamp to map bounds so border clicks stay valid */
1154 float map_max_fx2 = (float)map->width - 1e-4f;
1155 float map_max_fy2 = (float)map->height - 1e-4f;
1156 if (refined_fx < 0.0f) refined_fx = 0.0f;
1157 if (refined_fy < 0.0f) refined_fy = 0.0f;
1158 if (refined_fx > map_max_fx2) refined_fx = map_max_fx2;
1159 if (refined_fy > map_max_fy2) refined_fy = map_max_fy2;
1160 if (out_fx) *out_fx = refined_fx;
1161 if (out_fy) *out_fy = refined_fy;
1162 }
1163 return;
1164 }
1165 }
1166 }
1167
1168 /* Fallback: flat projection (height 0) */
1169 float fmx = screen_x / tw + screen_y / th - 1.0f;
1170 float fmy = screen_y / th - screen_x / tw + 1.0f;
1171 *mx = (int)floorf(fmx);
1172 *my = (int)floorf(fmy);
1173 if (out_fx) *out_fx = fmx;
1174 if (out_fy) *out_fy = fmy;
1175} /* iso_screen_to_map_height_f() */
1176
1185float iso_map_interpolate_height(const ISO_MAP* map, float fx, float fy) {
1186 __n_assert(map, return 0.0f);
1187
1188 /* Clamp to valid map range so border tiles interpolate correctly.
1189 * Allow the full extent of the last tile (up to width/height minus
1190 * a tiny epsilon) so that positions in the right half of border
1191 * tiles are not snapped to the tile corner. */
1192 if (fx < 0.0f) fx = 0.0f;
1193 if (fy < 0.0f) fy = 0.0f;
1194 float max_fx = (float)map->width - 1e-4f;
1195 float max_fy = (float)map->height - 1e-4f;
1196 if (fx > max_fx) fx = max_fx;
1197 if (fy > max_fy) fy = max_fy;
1198
1199 int ix = (int)floorf(fx);
1200 int iy = (int)floorf(fy);
1201 float frac_x = fx - (float)ix;
1202 float frac_y = fy - (float)iy;
1203
1204 /* Clamp neighbor indices for edge tiles */
1205 int ix1 = (ix + 1 < map->width) ? ix + 1 : ix;
1206 int iy1 = (iy + 1 < map->height) ? iy + 1 : iy;
1207
1208 float h00 = (float)iso_map_get_height(map, ix, iy);
1209 float h10 = (float)iso_map_get_height(map, ix1, iy);
1210 float h01 = (float)iso_map_get_height(map, ix, iy1);
1211 float h11 = (float)iso_map_get_height(map, ix1, iy1);
1212
1213 float top = h00 + (h10 - h00) * frac_x;
1214 float bot = h01 + (h11 - h01) * frac_x;
1215 return top + (bot - top) * frac_y;
1216} /* iso_map_interpolate_height() */
1217
1230void iso_map_calc_transitions(const ISO_MAP* map, int mx, int my, int* edge_bits, int* corner_bits) {
1231 __n_assert(map, return);
1232 __n_assert(edge_bits, return);
1233 __n_assert(corner_bits, return);
1234
1235 int center = iso_map_get_terrain(map, mx, my);
1236 *edge_bits = 0;
1237 *corner_bits = 0;
1238
1239 /* Edge neighbors: W(-1,0), N(0,-1), E(+1,0), S(0,+1) */
1240 int w = iso_map_get_terrain(map, mx - 1, my);
1241 int n = iso_map_get_terrain(map, mx, my - 1);
1242 int e = iso_map_get_terrain(map, mx + 1, my);
1243 int s = iso_map_get_terrain(map, mx, my + 1);
1244
1245 if (w != center) *edge_bits |= ISO_EDGE_W;
1246 if (n != center) *edge_bits |= ISO_EDGE_N;
1247 if (e != center) *edge_bits |= ISO_EDGE_E;
1248 if (s != center) *edge_bits |= ISO_EDGE_S;
1249
1250 /* Corner neighbors: only set if adjacent edges don't already have that terrain */
1251 int nw = iso_map_get_terrain(map, mx - 1, my - 1);
1252 int ne = iso_map_get_terrain(map, mx + 1, my - 1);
1253 int se = iso_map_get_terrain(map, mx + 1, my + 1);
1254 int sw = iso_map_get_terrain(map, mx - 1, my + 1);
1255
1256 if (nw != center && nw != w && nw != n) *corner_bits |= ISO_CORNER_NW;
1257 if (ne != center && ne != n && ne != e) *corner_bits |= ISO_CORNER_NE;
1258 if (se != center && se != e && se != s) *corner_bits |= ISO_CORNER_SE;
1259 if (sw != center && sw != s && sw != w) *corner_bits |= ISO_CORNER_SW;
1260} /* iso_map_calc_transitions() */
1261
1271int iso_map_should_transition(const ISO_MAP* map, int mx1, int my1, int mx2, int my2) {
1272 __n_assert(map, return 0);
1273 return iso_map_get_terrain(map, mx1, my1) != iso_map_get_terrain(map, mx2, my2);
1274} /* iso_map_should_transition() */
1275
1289void iso_map_corner_heights(const ISO_MAP* map, int mx, int my, float* h_n, float* h_e, float* h_s, float* h_w) {
1290 __n_assert(map, return);
1291 __n_assert(h_n, return);
1292 __n_assert(h_e, return);
1293 __n_assert(h_s, return);
1294 __n_assert(h_w, return);
1295
1296 float c = (float)iso_map_get_height(map, mx, my);
1297 float nw = (float)iso_map_get_height(map, mx - 1, my - 1);
1298 float nn = (float)iso_map_get_height(map, mx, my - 1);
1299 float ne = (float)iso_map_get_height(map, mx + 1, my - 1);
1300 float ee = (float)iso_map_get_height(map, mx + 1, my);
1301 float se = (float)iso_map_get_height(map, mx + 1, my + 1);
1302 float ss = (float)iso_map_get_height(map, mx, my + 1);
1303 float sw = (float)iso_map_get_height(map, mx - 1, my + 1);
1304 float ww = (float)iso_map_get_height(map, mx - 1, my);
1305
1306 *h_n = (c + nw + nn + ww) / 4.0f;
1307 *h_e = (c + nn + ne + ee) / 4.0f;
1308 *h_s = (c + ee + se + ss) / 4.0f;
1309 *h_w = (c + ss + sw + ww) / 4.0f;
1310} /* iso_map_corner_heights() */
1311
1320int iso_map_save(const ISO_MAP* map, const char* filename) {
1321 __n_assert(map, return FALSE);
1322 __n_assert(filename, return FALSE);
1323
1324 FILE* f = fopen(filename, "wb");
1325 if (!f) {
1326 n_log(LOG_ERR, "iso_map_save: could not open %s for writing", filename);
1327 return FALSE;
1328 }
1329
1330 /* magic */
1331 fwrite("ISOM", 1, 4, f);
1332
1333 /* header */
1334 const int32_t hdr[4] = {map->width, map->height, map->num_terrains, map->max_height};
1335 fwrite(hdr, sizeof(int32_t), 4, f);
1336
1337 size_t total = (size_t)map->width * (size_t)map->height;
1338
1339 /* terrain layer */
1340 fwrite(map->terrain, sizeof(int), total, f);
1341 /* heightmap layer */
1342 fwrite(map->heightmap, sizeof(int), total, f);
1343 /* ability layer */
1344 fwrite(map->ability, sizeof(int), total, f);
1345
1346 fclose(f);
1347 return TRUE;
1348} /* iso_map_save() */
1349
1355ISO_MAP* iso_map_load(const char* filename) {
1356 __n_assert(filename, return NULL);
1357
1358 FILE* f = fopen(filename, "rb");
1359 if (!f) {
1360 n_log(LOG_ERR, "iso_map_load: could not open %s for reading", filename);
1361 return NULL;
1362 }
1363
1364 /* check magic */
1365 char magic[4];
1366 if (fread(magic, 1, 4, f) != 4 ||
1367 magic[0] != 'I' || magic[1] != 'S' || magic[2] != 'O' || magic[3] != 'M') {
1368 n_log(LOG_ERR, "iso_map_load: invalid file format");
1369 fclose(f);
1370 return NULL;
1371 }
1372
1373 int32_t hdr[4];
1374 if (fread(hdr, sizeof(int32_t), 4, f) != 4) {
1375 n_log(LOG_ERR, "iso_map_load: truncated header");
1376 fclose(f);
1377 return NULL;
1378 }
1379
1380 ISO_MAP* map = iso_map_new(hdr[0], hdr[1], hdr[2], hdr[3]);
1381 if (!map) {
1382 fclose(f);
1383 return NULL;
1384 }
1385
1386 size_t total = (size_t)map->width * (size_t)map->height;
1387 size_t r = 0;
1388 r = fread(map->terrain, sizeof(int), total, f);
1389 if (r == total && !feof(f))
1390 r += fread(map->heightmap, sizeof(int), total, f);
1391 if (r == total * 2 && !feof(f))
1392 r += fread(map->ability, sizeof(int), total, f);
1393
1394 if (r != total * 3) {
1395 n_log(LOG_ERR, "iso_map_load: truncated data");
1396 iso_map_free(&map);
1397 fclose(f);
1398 return NULL;
1399 }
1400
1401 fclose(f);
1402 return map;
1403} /* iso_map_load() */
1404
1410 __n_assert(map, return);
1411 for (int y = 0; y < map->height; y++) {
1412 for (int x = 0; x < map->width; x++) {
1413 int idx = y * map->width + x;
1414 map->terrain[idx] = rand() % map->num_terrains;
1415 map->heightmap[idx] = rand() % (map->max_height + 1);
1416 map->ability[idx] = WALK;
1417 }
1418 }
1419} /* iso_map_randomize() */
1420
1426static inline float _smooth_neighbor_h(const ISO_MAP* map, float h_self, int nx, int ny) {
1427 float nh = (float)iso_map_get_height(map, nx, ny);
1428 if (fabsf(h_self - nh) > (float)map->smooth_slope_max) return h_self;
1429 return nh;
1430}
1431
1444void 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) {
1445 __n_assert(map, return);
1446 __n_assert(h_n, return);
1447 __n_assert(h_e, return);
1448 __n_assert(h_s, return);
1449 __n_assert(h_w, return);
1450
1451 float h = (float)iso_map_get_height(map, mx, my);
1452
1453 *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;
1454
1455 *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;
1456
1457 *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;
1458
1459 *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;
1460} /* iso_map_smooth_corner_heights() */
1461
1462/*---------------------------------------------------------------------------
1463 * Per-tile segment API
1464 *-------------------------------------------------------------------------*/
1465
1466int iso_map_set_segments(ISO_MAP* map, int mx, int my, const ISO_TILE_SEGMENT* segs, int count) {
1467 __n_assert(map, return 0);
1468 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return 0;
1469 if (count < 0 || count > ISO_MAX_SEGMENTS_PER_TILE) return 0;
1470
1471 /* Validate invariants */
1472 for (int i = 0; i < count; i++) {
1473 if (segs[i].bottom >= segs[i].top) return 0;
1474 if (i > 0 && segs[i - 1].top > segs[i].bottom) return 0;
1475 }
1476
1477 /* Allocate segment array on first use */
1478 if (!map->segments) {
1479 size_t total = (size_t)map->width * (size_t)map->height;
1480 Malloc(map->segments, ISO_TILE_SEGMENTS, total);
1481 if (!map->segments) return 0;
1482 memset(map->segments, 0, total * sizeof(ISO_TILE_SEGMENTS));
1483 }
1484
1485 int idx = my * map->width + mx;
1486 map->segments[idx].count = count;
1487 for (int i = 0; i < count; i++) map->segments[idx].segs[i] = segs[i];
1488 for (int i = count; i < ISO_MAX_SEGMENTS_PER_TILE; i++) {
1489 map->segments[idx].segs[i].bottom = 0;
1490 map->segments[idx].segs[i].top = 0;
1491 map->segments[idx].segs[i].upper_tile = -1;
1492 map->segments[idx].segs[i].lower_tile = -1;
1493 }
1494
1495 /* Update derived heightmap */
1496 map->heightmap[idx] = (count > 0) ? segs[count - 1].top : 0;
1497 return 1;
1498}
1499
1500const ISO_TILE_SEGMENTS* iso_map_get_segments(const ISO_MAP* map, int mx, int my) {
1501 if (!map || !map->segments) return NULL;
1502 if (mx < 0 || mx >= map->width || my < 0 || my >= map->height) return NULL;
1503 return &map->segments[my * map->width + mx];
1504}
1505
1506static int _cmp_draw_entry(const void* a, const void* b) {
1507 const ISO_DRAW_ENTRY* ea = (const ISO_DRAW_ENTRY*)a;
1508 const ISO_DRAW_ENTRY* eb = (const ISO_DRAW_ENTRY*)b;
1509 /* Primary: isometric depth (back-to-front) */
1510 int depth_a = ea->my + ea->mx;
1511 int depth_b = eb->my + eb->mx;
1512 if (depth_a != depth_b) return depth_a - depth_b;
1513 /* Secondary: segment base height (lower drawn first) */
1514 if (ea->bottom != eb->bottom) return ea->bottom - eb->bottom;
1515 /* Tertiary: row then column */
1516 if (ea->my != eb->my) return ea->my - eb->my;
1517 return ea->mx - eb->mx;
1518}
1519
1520int iso_map_build_draw_order(const ISO_MAP* map, ISO_DRAW_ENTRY* out, int max_entries) {
1521 __n_assert(map, return 0);
1522 __n_assert(out, return 0);
1523
1524 int n = 0;
1525 for (int my = 0; my < map->height; my++) {
1526 for (int mx = 0; mx < map->width; mx++) {
1527 int idx = my * map->width + mx;
1528 if (map->segments && map->segments[idx].count > 0) {
1529 const ISO_TILE_SEGMENTS* ts = &map->segments[idx];
1530 for (int si = 0; si < ts->count && n < max_entries; si++) {
1531 out[n].mx = mx;
1532 out[n].my = my;
1533 out[n].seg_idx = si;
1534 out[n].bottom = ts->segs[si].bottom;
1535 out[n].top = ts->segs[si].top;
1536 /* underside: bottom > 0 and no segment directly below */
1537 out[n].underside = (ts->segs[si].bottom > 0 &&
1538 (si == 0 || ts->segs[si - 1].top < ts->segs[si].bottom));
1539 n++;
1540 }
1541 } else {
1542 /* No segments or count==0: single entry from heightmap */
1543 if (n >= max_entries) continue;
1544 int h = map->heightmap[idx];
1545 out[n].mx = mx;
1546 out[n].my = my;
1547 out[n].seg_idx = 0;
1548 out[n].bottom = 0;
1549 out[n].top = h;
1550 out[n].underside = 0;
1551 n++;
1552 }
1553 }
1554 }
1555
1556 qsort(out, (size_t)n, sizeof(ISO_DRAW_ENTRY), _cmp_draw_entry);
1557 return n;
1558}
1559
1566int iso_map_should_transition_smooth(const ISO_MAP* map, int mx1, int my1, int mx2, int my2) {
1567 __n_assert(map, return 0);
1568 int h1 = iso_map_get_height(map, mx1, my1);
1569 int h2 = iso_map_get_height(map, mx2, my2);
1570 if (map->smooth_height)
1571 return abs(h1 - h2) <= map->smooth_slope_max;
1572 return h1 == h2;
1573} /* iso_map_should_transition_smooth() */
1574
1586void iso_map_calc_transitions_full(const ISO_MAP* map, int mx, int my, int* edge_bits, int* corner_bits) {
1587 __n_assert(map, return);
1588 __n_assert(edge_bits, return);
1589 __n_assert(corner_bits, return);
1590
1591 int base = iso_map_get_terrain(map, mx, my);
1592 memset(edge_bits, 0, sizeof(int) * (size_t)map->num_terrains);
1593 memset(corner_bits, 0, sizeof(int) * (size_t)map->num_terrains);
1594
1595 /* Cardinal neighbors: only if height-connected */
1596 int t_w = iso_map_should_transition_smooth(map, mx, my, mx - 1, my)
1597 ? iso_map_get_terrain(map, mx - 1, my)
1598 : base;
1599 int t_n = iso_map_should_transition_smooth(map, mx, my, mx, my - 1)
1600 ? iso_map_get_terrain(map, mx, my - 1)
1601 : base;
1602 int t_e = iso_map_should_transition_smooth(map, mx, my, mx + 1, my)
1603 ? iso_map_get_terrain(map, mx + 1, my)
1604 : base;
1605 int t_s = iso_map_should_transition_smooth(map, mx, my, mx, my + 1)
1606 ? iso_map_get_terrain(map, mx, my + 1)
1607 : base;
1608
1609 /* Diagonal neighbors */
1610 int t_nw = iso_map_should_transition_smooth(map, mx, my, mx - 1, my - 1)
1611 ? iso_map_get_terrain(map, mx - 1, my - 1)
1612 : base;
1613 int t_ne = iso_map_should_transition_smooth(map, mx, my, mx + 1, my - 1)
1614 ? iso_map_get_terrain(map, mx + 1, my - 1)
1615 : base;
1616 int t_se = iso_map_should_transition_smooth(map, mx, my, mx + 1, my + 1)
1617 ? iso_map_get_terrain(map, mx + 1, my + 1)
1618 : base;
1619 int t_sw = iso_map_should_transition_smooth(map, mx, my, mx - 1, my + 1)
1620 ? iso_map_get_terrain(map, mx - 1, my + 1)
1621 : base;
1622
1623 for (int t = base + 1; t < map->num_terrains; t++) {
1624 if (t_w == t) edge_bits[t] |= ISO_EDGE_W;
1625 if (t_n == t) edge_bits[t] |= ISO_EDGE_N;
1626 if (t_e == t) edge_bits[t] |= ISO_EDGE_E;
1627 if (t_s == t) edge_bits[t] |= ISO_EDGE_S;
1628
1629 if (t_nw == t && !(edge_bits[t] & ISO_EDGE_W) && !(edge_bits[t] & ISO_EDGE_N))
1630 corner_bits[t] |= ISO_CORNER_NW;
1631 if (t_ne == t && !(edge_bits[t] & ISO_EDGE_N) && !(edge_bits[t] & ISO_EDGE_E))
1632 corner_bits[t] |= ISO_CORNER_NE;
1633 if (t_se == t && !(edge_bits[t] & ISO_EDGE_E) && !(edge_bits[t] & ISO_EDGE_S))
1634 corner_bits[t] |= ISO_CORNER_SE;
1635 if (t_sw == t && !(edge_bits[t] & ISO_EDGE_S) && !(edge_bits[t] & ISO_EDGE_W))
1636 corner_bits[t] |= ISO_CORNER_SW;
1637 }
1638} /* iso_map_calc_transitions_full() */
1639
1640/*===========================================================================
1641 * N_ISO_CAMERA
1642 *=========================================================================*/
1643
1647N_ISO_CAMERA* n_iso_camera_new(float zoom_min, float zoom_max) {
1648 N_ISO_CAMERA* cam = NULL;
1649 Malloc(cam, N_ISO_CAMERA, 1);
1650 __n_assert(cam, return NULL);
1651 cam->x = 0.0f;
1652 cam->y = 0.0f;
1653 cam->zoom = 1.0f;
1654 cam->zoom_min = zoom_min;
1655 cam->zoom_max = zoom_max;
1656 return cam;
1657} /* n_iso_camera_new() */
1658
1663 __n_assert(cam && *cam, return);
1664 Free(*cam);
1665} /* n_iso_camera_free() */
1666
1670void n_iso_camera_scroll(N_ISO_CAMERA* cam, float dx, float dy) {
1671 __n_assert(cam, return);
1672 cam->x += dx;
1673 cam->y += dy;
1674} /* n_iso_camera_scroll() */
1675
1681void n_iso_camera_zoom(N_ISO_CAMERA* cam, float dz, float mouse_x, float mouse_y) {
1682 __n_assert(cam, return);
1683 float old_zoom = cam->zoom;
1684 cam->zoom += dz;
1685 if (cam->zoom < cam->zoom_min) cam->zoom = cam->zoom_min;
1686 if (cam->zoom > cam->zoom_max) cam->zoom = cam->zoom_max;
1687 /* Adjust position so the world point under the mouse stays fixed */
1688 cam->x += mouse_x / cam->zoom - mouse_x / old_zoom;
1689 cam->y += mouse_y / cam->zoom - mouse_y / old_zoom;
1690} /* n_iso_camera_zoom() */
1691
1695void n_iso_camera_center_on(N_ISO_CAMERA* cam, float world_x, float world_y, int screen_w, int screen_h) {
1696 __n_assert(cam, return);
1697 cam->x = -world_x + (float)screen_w / (2.0f * cam->zoom);
1698 cam->y = -world_y + (float)screen_h / (2.0f * cam->zoom);
1699} /* n_iso_camera_center_on() */
1700
1704void n_iso_camera_follow(N_ISO_CAMERA* cam, float target_x, float target_y, int screen_w, int screen_h, float smoothing, float dt) {
1705 __n_assert(cam, return);
1706 float target_cx = -target_x + (float)screen_w / (2.0f * cam->zoom);
1707 float target_cy = -target_y + (float)screen_h / (2.0f * cam->zoom);
1708 float t = smoothing * dt;
1709 if (t > 1.0f) t = 1.0f;
1710 cam->x += (target_cx - cam->x) * t;
1711 cam->y += (target_cy - cam->y) * t;
1712} /* n_iso_camera_follow() */
1713
1717void n_iso_camera_screen_to_world(const N_ISO_CAMERA* cam, float sx, float sy, float* wx, float* wy) {
1718 __n_assert(cam, return);
1719 __n_assert(wx, return);
1720 __n_assert(wy, return);
1721 *wx = sx / cam->zoom - cam->x;
1722 *wy = sy / cam->zoom - cam->y;
1723} /* n_iso_camera_screen_to_world() */
1724
1728void n_iso_camera_world_to_screen(const N_ISO_CAMERA* cam, float wx, float wy, float* sx, float* sy) {
1729 __n_assert(cam, return);
1730 __n_assert(sx, return);
1731 __n_assert(sy, return);
1732 *sx = (wx + cam->x) * cam->zoom;
1733 *sy = (wy + cam->y) * cam->zoom;
1734} /* n_iso_camera_world_to_screen() */
1735
1736/*---------------------------------------------------------------------------
1737 * Height border edge visibility (public API, no Allegro dependency)
1738 *-------------------------------------------------------------------------*/
1739
1753 ISO_VISIBLE_EDGES edges = {0};
1754 if (!map) return edges;
1755
1756 int h = iso_map_get_height(map, mx, my);
1757 if (h == 0) return edges;
1758
1759 /* NW edge: neighbor (mx-1, my) */
1760 if (mx <= 0) {
1761 /* At west boundary: use neighbor chunk data if available */
1762 if (map->neighbor_heights_west) {
1763 if (abs(h - map->neighbor_heights_west[my]) >= 1)
1764 edges.draw_nw = 1;
1765 } else {
1766 edges.draw_nw = 1; /* world boundary */
1767 }
1768 } else if (abs(h - iso_map_get_height(map, mx - 1, my)) >= 1) {
1769 edges.draw_nw = 1;
1770 }
1771
1772 /* NE edge: neighbor (mx, my-1) */
1773 if (my <= 0) {
1774 if (map->neighbor_heights_north) {
1775 if (abs(h - map->neighbor_heights_north[mx]) >= 1)
1776 edges.draw_ne = 1;
1777 } else {
1778 edges.draw_ne = 1;
1779 }
1780 } else if (abs(h - iso_map_get_height(map, mx, my - 1)) >= 1) {
1781 edges.draw_ne = 1;
1782 }
1783
1784 /* SE edge: neighbor (mx+1, my) */
1785 if (mx >= map->width - 1) {
1786 if (map->neighbor_heights_east) {
1787 if (abs(h - map->neighbor_heights_east[my]) >= 1)
1788 edges.draw_se = 1;
1789 } else {
1790 edges.draw_se = 1;
1791 }
1792 } else if (abs(h - iso_map_get_height(map, mx + 1, my)) >= 1) {
1793 edges.draw_se = 1;
1794 }
1795
1796 /* SW edge: neighbor (mx, my+1) */
1797 if (my >= map->height - 1) {
1798 if (map->neighbor_heights_south) {
1799 if (abs(h - map->neighbor_heights_south[mx]) >= 1)
1800 edges.draw_sw = 1;
1801 } else {
1802 edges.draw_sw = 1;
1803 }
1804 } else if (abs(h - iso_map_get_height(map, mx, my + 1)) >= 1) {
1805 edges.draw_sw = 1;
1806 }
1807
1808 return edges;
1809}
1810
1824static int _tile_covers_height(const ISO_MAP* map, int mx, int my, int z) {
1825 if (!map || mx < 0 || mx >= map->width || my < 0 || my >= map->height)
1826 return 0;
1827 const ISO_TILE_SEGMENTS* ts = iso_map_get_segments(map, mx, my);
1828 if (ts && ts->count > 0) {
1829 for (int i = 0; i < ts->count; i++) {
1830 if (ts->segs[i].bottom <= z && ts->segs[i].top > z + 1)
1831 return 1;
1832 }
1833 return 0;
1834 }
1835 int h = iso_map_get_height(map, mx, my);
1836 return (h > z + 1) ? 1 : 0;
1837}
1838
1863ISO_VISIBLE_EDGES iso_map_get_visible_edges_segment(const ISO_MAP* map, int mx, int my, int seg_idx) {
1864 ISO_VISIBLE_EDGES edges = {0};
1865 if (!map) return edges;
1866
1867 /* Get this tile's segment */
1868 ISO_TILE_SEGMENT seg;
1869 const ISO_TILE_SEGMENTS* ts = iso_map_get_segments(map, mx, my);
1870 if (ts && seg_idx >= 0 && seg_idx < ts->count) {
1871 seg = ts->segs[seg_idx];
1872 } else {
1873 /* Fallback: heightmap as single segment {0, h} */
1874 int h = iso_map_get_height(map, mx, my);
1875 if (h == 0) return edges;
1876 seg.bottom = 0;
1877 seg.top = h;
1878 }
1879
1880 if (seg.top <= seg.bottom) return edges;
1881
1882 /* Direction offsets: NW→(mx-1,my), NE→(mx,my-1), SE→(mx+1,my), SW→(mx,my+1) */
1883 static const int ddx[4] = {-1, 0, 1, 0};
1884 static const int ddy[4] = {0, -1, 0, 1};
1885 int* flags[4];
1886 flags[0] = &edges.draw_nw;
1887 flags[1] = &edges.draw_ne;
1888 flags[2] = &edges.draw_se;
1889 flags[3] = &edges.draw_sw;
1890
1891 int any_side_exposed = 0; /* for underside check */
1892 int ntop[4] = {0, 0, 0, 0}; /* neighbor top height per direction */
1893
1894 for (int d = 0; d < 4; d++) {
1895 int nx = mx + ddx[d];
1896 int ny = my + ddy[d];
1897 int neighbor_matches = 0;
1898 int neighbor_covers_bottom = 0;
1899
1900 if (nx < 0 || nx >= map->width || ny < 0 || ny >= map->height) {
1901 /* Boundary: check neighbor chunk height arrays */
1902 int nh = -1;
1903 if (d == 0 && map->neighbor_heights_west) nh = map->neighbor_heights_west[my];
1904 if (d == 1 && map->neighbor_heights_north) nh = map->neighbor_heights_north[mx];
1905 if (d == 2 && map->neighbor_heights_east) nh = map->neighbor_heights_east[my];
1906 if (d == 3 && map->neighbor_heights_south) nh = map->neighbor_heights_south[mx];
1907
1908 if (nh > 0) {
1909 /* Boundary treated as single segment {0,nh}: match if same top */
1910 neighbor_matches = (nh == seg.top);
1911 neighbor_covers_bottom = (nh >= seg.bottom);
1912 ntop[d] = nh;
1913 }
1914 /* else: world boundary → no match, not covered, ntop stays 0 */
1915 } else {
1916 const ISO_TILE_SEGMENTS* nts = iso_map_get_segments(map, nx, ny);
1917 if (nts && nts->count > 0) {
1918 /* Match if any neighbour segment has the same top height.
1919 * This applies uniformly to ground and floating segments:
1920 * same top = connected surface = no border.
1921 * Different top = visible height step = border drawn. */
1922 for (int si = 0; si < nts->count; si++) {
1923 if (nts->segs[si].top == seg.top) {
1924 neighbor_matches = 1;
1925 }
1926 if (nts->segs[si].top >= seg.bottom) {
1927 neighbor_covers_bottom = 1;
1928 }
1929 }
1930 /* Use heightmap value as neighbor top for vertical lines */
1931 ntop[d] = iso_map_get_height(map, nx, ny);
1932 } else {
1933 /* No segment data: heightmap as single segment {0, h} */
1934 int nh = iso_map_get_height(map, nx, ny);
1935 if (nh > 0) {
1936 neighbor_matches = (nh == seg.top);
1937 neighbor_covers_bottom = (nh >= seg.bottom);
1938 }
1939 ntop[d] = nh;
1940 }
1941 }
1942
1943 if (!neighbor_matches) *flags[d] = 1;
1944 if (!neighbor_covers_bottom) any_side_exposed = 1;
1945 }
1946
1947 /* OCCLUSION: NW/NE edges face away from camera.
1948 * Suppress if the tile drawn later (south in painter order) has
1949 * geometry covering this segment's top face height.
1950 * NW edge: covered by SW neighbour (mx, my+1)
1951 * NE edge: covered by SE neighbour (mx+1, my) */
1952 if (edges.draw_nw && _tile_covers_height(map, mx, my + 1, seg.top))
1953 edges.draw_nw = 0;
1954 if (edges.draw_ne && _tile_covers_height(map, mx + 1, my, seg.top))
1955 edges.draw_ne = 0;
1956
1957 /* Underside border for floating segments */
1958 if (seg.bottom > 0 && any_side_exposed) {
1959 edges.draw_underside = 1;
1960 }
1961
1962 /* Store neighbor top heights for vertical corner line computation */
1963 edges.neighbor_top_nw = ntop[0];
1964 edges.neighbor_top_ne = ntop[1];
1965 edges.neighbor_top_se = ntop[2];
1966 edges.neighbor_top_sw = ntop[3];
1967
1968 return edges;
1969}
1970
1971/*===========================================================================
1972 * ISO_MAP rendering (requires Allegro 5)
1973 *=========================================================================*/
1974#ifdef HAVE_ALLEGRO
1975
1976#include <allegro5/allegro.h>
1977#include <allegro5/allegro_primitives.h>
1978
2004static 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) {
2005 float wx = (float)(cx - cy) * hw + 2.0f * hw;
2006 float wy = (float)(cx + cy) * hh - fh * tl;
2007 *sx = wx * zoom + cam_px;
2008 *sy = wy * zoom + cam_py;
2009}
2010
2019static inline ALLEGRO_COLOR _height_tint(int seg_top, int max_height, float intensity) {
2020 if (intensity <= 0.0f || max_height <= 0)
2021 return al_map_rgba_f(1.0f, 1.0f, 1.0f, 1.0f);
2022 float ratio = (float)seg_top / (float)max_height;
2023 /* brightness ranges from (1 - intensity) at height 0 to 1.0 at max_height */
2024 float b = 1.0f - intensity * (1.0f - ratio);
2025 if (b < 0.0f) b = 0.0f;
2026 if (b > 1.0f) b = 1.0f;
2027 return al_map_rgba_f(b, b, b, 1.0f);
2028}
2029
2030/* Extended tint: combines height tint with ambient color and per-tile dynamic light.
2031 * ambient_r/g/b: global day/night ambient (1.0 = full daylight).
2032 * dyn_r/g/b: additive dynamic light contribution at this tile (0.0 = no light). */
2033static 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) {
2034 /* Start with height-based brightness */
2035 float hb = 1.0f;
2036 if (intensity > 0.0f && max_height > 0) {
2037 float ratio = (float)seg_top / (float)max_height;
2038 hb = 1.0f - intensity * (1.0f - ratio);
2039 if (hb < 0.0f) hb = 0.0f;
2040 if (hb > 1.0f) hb = 1.0f;
2041 }
2042
2043 /* Multiply by ambient color */
2044 float r = hb * ambient_r;
2045 float g = hb * ambient_g;
2046 float b = hb * ambient_b;
2047
2048 /* Add dynamic light (additive boost) */
2049 r += dyn_r;
2050 g += dyn_g;
2051 b += dyn_b;
2052
2053 /* Clamp to avoid overexposure */
2054 if (r > 1.0f) r = 1.0f;
2055 if (g > 1.0f) g = 1.0f;
2056 if (b > 1.0f) b = 1.0f;
2057
2058 return al_map_rgba_f(r, g, b, 1.0f);
2059}
2060
2067static void _draw_tile_warped(ALLEGRO_BITMAP* bmp,
2068 const float vx[4],
2069 const float vy[4],
2070 int tile_w,
2071 int tile_h,
2072 ALLEGRO_COLOR tint) {
2073 float un = (float)tile_w / 2.0f, vn = 0.0f;
2074 float ue = (float)tile_w, ve = (float)tile_h / 2.0f;
2075 float us = (float)tile_w / 2.0f, vs = (float)tile_h;
2076 float uw = 0.0f, vw = (float)tile_h / 2.0f;
2077 ALLEGRO_VERTEX vtx[6];
2078 vtx[0].x = vx[0];
2079 vtx[0].y = vy[0];
2080 vtx[0].z = 0;
2081 vtx[0].u = un;
2082 vtx[0].v = vn;
2083 vtx[0].color = tint;
2084 vtx[1].x = vx[1];
2085 vtx[1].y = vy[1];
2086 vtx[1].z = 0;
2087 vtx[1].u = ue;
2088 vtx[1].v = ve;
2089 vtx[1].color = tint;
2090 vtx[2].x = vx[2];
2091 vtx[2].y = vy[2];
2092 vtx[2].z = 0;
2093 vtx[2].u = us;
2094 vtx[2].v = vs;
2095 vtx[2].color = tint;
2096 vtx[3].x = vx[0];
2097 vtx[3].y = vy[0];
2098 vtx[3].z = 0;
2099 vtx[3].u = un;
2100 vtx[3].v = vn;
2101 vtx[3].color = tint;
2102 vtx[4].x = vx[2];
2103 vtx[4].y = vy[2];
2104 vtx[4].z = 0;
2105 vtx[4].u = us;
2106 vtx[4].v = vs;
2107 vtx[4].color = tint;
2108 vtx[5].x = vx[3];
2109 vtx[5].y = vy[3];
2110 vtx[5].z = 0;
2111 vtx[5].u = uw;
2112 vtx[5].v = vw;
2113 vtx[5].color = tint;
2114
2115 al_draw_prim(vtx, NULL, bmp, 0, 6, ALLEGRO_PRIM_TRIANGLE_LIST);
2116}
2117
2123static void _draw_segment_sides(const ISO_MAP* map,
2124 float east_x,
2125 float east_y,
2126 float south_x,
2127 float south_y,
2128 float west_x,
2129 float west_y,
2130 int mx,
2131 int my,
2132 int seg_bottom,
2133 int seg_top,
2134 float lift,
2135 float height_brightness,
2136 int wall_terrain_override) {
2137 int base = (wall_terrain_override >= 0) ? wall_terrain_override
2138 : iso_map_get_terrain(map, mx, my);
2139 float east_shade = 0.55f;
2140 float south_shade = 0.35f;
2141
2142 float cr, cg, cb;
2143 switch (base) {
2144 case 0:
2145 cr = 0.7f;
2146 cg = 0.25f;
2147 cb = 0.1f;
2148 break;
2149 case 1:
2150 cr = 0.5f;
2151 cg = 0.45f;
2152 cb = 0.35f;
2153 break;
2154 case 2:
2155 cr = 0.6f;
2156 cg = 0.55f;
2157 cb = 0.45f;
2158 break;
2159 default:
2160 cr = 0.4f;
2161 cg = 0.35f;
2162 cb = 0.3f;
2163 break;
2164 }
2165 ALLEGRO_COLOR east_col = al_map_rgba_f(cr * east_shade * height_brightness,
2166 cg * east_shade * height_brightness,
2167 cb * east_shade * height_brightness, 1.0f);
2168 ALLEGRO_COLOR south_col = al_map_rgba_f(cr * south_shade * height_brightness,
2169 cg * south_shade * height_brightness,
2170 cb * south_shade * height_brightness, 1.0f);
2171 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2172
2173 int east_face_h, south_face_h;
2174 if (seg_bottom == 0) {
2175 /* Ground segment: use neighbor-aware wall height (existing logic) */
2176 int h_e = iso_map_get_height(map, mx + 1, my);
2177 int h_s = iso_map_get_height(map, mx, my + 1);
2178 int h_se = iso_map_get_height(map, mx + 1, my + 1);
2179
2180 east_face_h = seg_top - (h_e < h_se ? h_e : h_se);
2181 // cppcheck-suppress redundantAssignment -- fallback chain: min(h_e,h_se) -> h_e -> h_se
2182 if (east_face_h < 1) east_face_h = seg_top - h_e;
2183 if (east_face_h < 1) east_face_h = seg_top - h_se;
2184
2185 south_face_h = seg_top - (h_s < h_se ? h_s : h_se);
2186 // cppcheck-suppress redundantAssignment -- fallback chain: min(h_s,h_se) -> h_s -> h_se
2187 if (south_face_h < 1) south_face_h = seg_top - h_s;
2188 if (south_face_h < 1) south_face_h = seg_top - h_se;
2189 } else {
2190 /* Floating segment: full wall from bottom to top */
2191 east_face_h = seg_top - seg_bottom;
2192 south_face_h = seg_top - seg_bottom;
2193 }
2194
2195 /* No sub-pixel expansion needed: all vertices come from the canonical
2196 * _iso_corner_to_screen formula, so adjacent tiles sharing an edge
2197 * produce bit-identical vertex positions. */
2198
2199 if (east_face_h > 0) {
2200 float drop = (float)east_face_h * lift;
2201 float v[8] = {east_x, east_y, south_x, south_y,
2202 south_x, south_y + drop,
2203 east_x, east_y + drop};
2204 al_draw_filled_polygon(v, 4, east_col);
2205 ALLEGRO_COLOR edge = al_map_rgba(0, 0, 0, 80);
2206 al_draw_line(east_x, east_y + drop, south_x, south_y + drop, edge, 1.0f);
2207 }
2208
2209 if (south_face_h > 0) {
2210 float drop = (float)south_face_h * lift;
2211 float v[8] = {south_x, south_y, west_x, west_y,
2212 west_x, west_y + drop,
2213 south_x, south_y + drop};
2214 al_draw_filled_polygon(v, 4, south_col);
2215 ALLEGRO_COLOR edge = al_map_rgba(0, 0, 0, 80);
2216 al_draw_line(west_x, west_y + drop, south_x, south_y + drop, edge, 1.0f);
2217 }
2218}
2219
2224static void _draw_segment_underside(ALLEGRO_BITMAP* bmp,
2225 float base_dx,
2226 float base_dy,
2227 float phw,
2228 float phh,
2229 float tile_draw_w,
2230 float tile_draw_h,
2231 int tile_w,
2232 int tile_h) {
2233 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2234 /* Draw the tile bitmap with a darkened tint (65% brightness) */
2235 ALLEGRO_COLOR tint = al_map_rgba_f(0.65f, 0.65f, 0.65f, 1.0f);
2236 (void)phw;
2237 (void)phh;
2238 al_draw_tinted_scaled_bitmap(bmp, tint,
2239 0, 0, (float)tile_w, (float)tile_h,
2240 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2241}
2242
2260 float obj_fx,
2261 float obj_fy,
2262 float obj_fz,
2263 float obj_sx,
2264 float obj_sy,
2265 float sprite_h,
2266 float sprite_half_w,
2267 float cam_px,
2268 float cam_py,
2269 float zoom,
2270 float* out_clip_y) {
2271 float phw = map->proj.half_w * zoom;
2272 float phh = map->proj.half_h * zoom;
2273 float lift = map->proj.tile_lift * zoom;
2274
2275 /* Entity head screen Y: sprite_h world units above feet */
2276 float head_sy = obj_sy - sprite_h * lift;
2277 /* Entity horizontal extent (screen pixels) */
2278 float ent_left = obj_sx - sprite_half_w * zoom;
2279 float ent_right = obj_sx + sprite_half_w * zoom;
2280
2281 int base_mx = (int)floorf(obj_fx);
2282 int base_my = (int)floorf(obj_fy);
2283 int check_range = map->max_height + 2;
2284 int found = 0;
2285 float best_clip_y = 1e9f;
2286
2287 for (int dy = -1; dy <= check_range; dy++) {
2288 for (int dx = -1; dx <= check_range; dx++) {
2289 /* Only check tiles drawn after the object in painter's order.
2290 * Filter by isometric depth (dx+dy) not just grid position. */
2291 int tdepth = dx + dy;
2292 if (tdepth < 0) continue;
2293 if (tdepth == 0 && dy < 0) continue;
2294 if (tdepth == 0 && dy == 0 && dx <= 0) continue;
2295
2296 int tmx = base_mx + dx;
2297 int tmy = base_my + dy;
2298 if (tmx < 0 || tmx >= map->width || tmy < 0 || tmy >= map->height) continue;
2299
2300 int th = iso_map_get_height(map, tmx, tmy);
2301 /* tile must reach above the entity's feet to potentially occlude */
2302 if ((float)th <= obj_fz) continue;
2303
2304 /* compute tile screen position (cam_px already snapped) */
2305 float tsx, tsy;
2306 iso_map_to_screen(map, tmx, tmy, th, &tsx, &tsy);
2307 float tile_dx = tsx * zoom + cam_px;
2308 float tile_dy = tsy * zoom + cam_py;
2309
2310 /* tile screen bounding box */
2311 float tile_left = tile_dx;
2312 float tile_right = tile_dx + 2.0f * phw;
2313 float tile_top = tile_dy;
2314 float tile_bot = tile_dy + 2.0f * phh + (float)th * lift;
2315
2316 /* Check horizontal overlap (entity range vs tile range) */
2317 if (ent_right < tile_left || ent_left > tile_right) continue;
2318
2319 /* Check vertical overlap (any part of entity behind tile) */
2320 if (tile_top <= obj_sy && tile_bot >= head_sy) {
2321 found = 1;
2322 /* Track north vertex of occluding tile for clip-Y */
2323 float nx, ny;
2325 map->proj.tile_lift, tmx, tmy, (float)th,
2326 cam_px, cam_py, zoom, &nx, &ny);
2327 if (ny < best_clip_y) best_clip_y = ny;
2328 }
2329 }
2330 }
2331 if (out_clip_y) *out_clip_y = best_clip_y;
2332 return found;
2333}
2334
2348static void _draw_height_borders(const ISO_MAP* map,
2349 int mx,
2350 int my,
2351 int seg_idx,
2352 int seg_bottom,
2353 int seg_top,
2354 int has_underside_face,
2355 const float vx[4],
2356 const float vy[4],
2357 float lift) {
2358 ISO_VISIBLE_EDGES edges = iso_map_get_visible_edges_segment(map, mx, my, seg_idx);
2359 if (!edges.draw_nw && !edges.draw_ne && !edges.draw_se && !edges.draw_sw && !(has_underside_face && edges.draw_underside))
2360 return;
2361
2362 ALLEGRO_COLOR border_col = al_map_rgba(0, 0, 0, 200);
2363 float thickness = 1.5f;
2364
2365 /* Top face edges */
2366 if (edges.draw_se)
2367 al_draw_line(vx[1], vy[1], vx[2], vy[2], border_col, thickness);
2368 if (edges.draw_sw)
2369 al_draw_line(vx[2], vy[2], vx[3], vy[3], border_col, thickness);
2370 if (edges.draw_nw)
2371 al_draw_line(vx[0], vy[0], vx[3], vy[3], border_col, thickness);
2372 if (edges.draw_ne)
2373 al_draw_line(vx[0], vy[0], vx[1], vy[1], border_col, thickness);
2374
2375 /* Vertical corner lines.
2376 *
2377 * For each drawn edge where seg_top > neighbor_top (A is higher):
2378 * Draw a vertical line at BOTH endpoints of that edge,
2379 * from tileToScreen(vertex, seg_top) down to
2380 * tileToScreen(vertex, neighbor_top).
2381 * Exception: NEVER draw at the North vertex (vx[0]/vy[0]).
2382 *
2383 * If seg_top <= neighbor_top: no vertical lines (wall belongs to N).
2384 *
2385 * vx[]/vy[] are already at tileToScreen(vertex, seg_top).
2386 * The bottom endpoint is (seg_top - HN) * lift pixels below.
2387 *
2388 * Diamond vertices: N=0, E=1, S=2, W=3.
2389 * Edge endpoints: NE=(N,E) SE=(E,S) SW=(S,W) NW=(W,N)
2390 *
2391 * Multiple edges may want to draw at the same vertex with different
2392 * drops. We draw the longest line (smallest HN) at each vertex
2393 * to avoid duplicate shorter lines.
2394 */
2395 {
2396 /* Per-vertex: track the largest drop (longest line) requested.
2397 *
2398 * CONTINUITY CHECK: a vertical corner line at a vertex is only
2399 * drawn if the wall face ENDS at that vertex — i.e. the adjacent
2400 * tile in the wall's direction is NOT at the same height.
2401 * If the adjacent tile IS at the same height, the wall surface
2402 * continues past the vertex and the line is interior (hidden).
2403 *
2404 * For the east-facing wall (SE edge):
2405 * E vertex: wall continues northward if NE neighbor same height → skip
2406 * S vertex: wall continues southward if SW neighbor same height → skip
2407 * For the south-facing wall (SW edge):
2408 * S vertex: wall continues eastward if SE neighbor same height → skip
2409 * W vertex: wall continues westward if NW neighbor same height → skip
2410 * For NE edge → E vertex: wall continues if SE neighbor same height
2411 * For NW edge → W vertex: wall continues if SW neighbor same height
2412 */
2413 float drop_e = 0, drop_s = 0, drop_w = 0;
2414 int nt_nw = edges.neighbor_top_nw;
2415 int nt_ne = edges.neighbor_top_ne;
2416 int nt_se = edges.neighbor_top_se;
2417 int nt_sw = edges.neighbor_top_sw;
2418
2419 /* Clamp neighbor tops to seg_bottom: vertical lines must not
2420 * extend below the segment's own base (floating segments). */
2421 int bot = seg_bottom;
2422 int cl_nw = nt_nw > bot ? nt_nw : bot;
2423 int cl_ne = nt_ne > bot ? nt_ne : bot;
2424 int cl_se = nt_se > bot ? nt_se : bot;
2425 int cl_sw = nt_sw > bot ? nt_sw : bot;
2426
2427 /* NE edge (N,E): only E vertex (N is never drawn) */
2428 if (edges.draw_ne && seg_top > cl_ne) {
2429 /* E vertex: east wall continues if SE neighbor same height */
2430 if (nt_se != seg_top) {
2431 float d = (float)(seg_top - cl_ne) * lift;
2432 if (d > drop_e) drop_e = d;
2433 }
2434 }
2435 /* SE edge (E,S): E and S vertices */
2436 if (edges.draw_se && seg_top > cl_se) {
2437 float d = (float)(seg_top - cl_se) * lift;
2438 /* E vertex: east wall continues northward if NE neighbor same height */
2439 if (nt_ne != seg_top) {
2440 if (d > drop_e) drop_e = d;
2441 }
2442 /* S vertex: east wall continues southward if SW neighbor same height */
2443 if (nt_sw != seg_top) {
2444 if (d > drop_s) drop_s = d;
2445 }
2446 }
2447 /* SW edge (S,W): S and W vertices */
2448 if (edges.draw_sw && seg_top > cl_sw) {
2449 float d = (float)(seg_top - cl_sw) * lift;
2450 /* S vertex: south wall continues eastward if SE neighbor same height */
2451 if (nt_se != seg_top) {
2452 if (d > drop_s) drop_s = d;
2453 }
2454 /* W vertex: south wall continues westward if NW neighbor same height */
2455 if (nt_nw != seg_top) {
2456 if (d > drop_w) drop_w = d;
2457 }
2458 }
2459 /* NW edge (W,N): only W vertex (N is never drawn) */
2460 if (edges.draw_nw && seg_top > cl_nw) {
2461 /* W vertex: south wall continues if SW neighbor same height */
2462 if (nt_sw != seg_top) {
2463 float d = (float)(seg_top - cl_nw) * lift;
2464 if (d > drop_w) drop_w = d;
2465 }
2466 }
2467
2468 if (drop_e > 0)
2469 al_draw_line(vx[1], vy[1], vx[1], vy[1] + drop_e, border_col, thickness);
2470 if (drop_s > 0)
2471 al_draw_line(vx[2], vy[2], vx[2], vy[2] + drop_s, border_col, thickness);
2472 if (drop_w > 0)
2473 al_draw_line(vx[3], vy[3], vx[3], vy[3] + drop_w, border_col, thickness);
2474 }
2475
2476 /* Underside face border (floating segments only).
2477 * The underside diamond is at seg_bottom, offset below the top face
2478 * by (seg_top - seg_bottom) * lift pixels. Only drawn if the
2479 * underside face itself was rendered (has_underside_face).
2480 * Only SE and SW edges — NW and NE are always hidden behind the
2481 * segment's own side walls from the camera angle. */
2482 if (has_underside_face && edges.draw_underside) {
2483 float dy = (float)(seg_top - seg_bottom) * lift;
2484 /* SE edge */
2485 al_draw_line(vx[1], vy[1] + dy, vx[2], vy[2] + dy, border_col, thickness);
2486 /* SW edge */
2487 al_draw_line(vx[2], vy[2] + dy, vx[3], vy[3] + dy, border_col, thickness);
2488 }
2489}
2490
2510void iso_map_draw(const ISO_MAP* map,
2511 ALLEGRO_BITMAP** tile_bitmaps,
2512 ALLEGRO_BITMAP*** transition_tiles,
2513 int num_masks,
2514 ALLEGRO_BITMAP** overlay_bitmaps,
2515 int num_overlay_tiles,
2516 float cam_px,
2517 float cam_py,
2518 float zoom,
2519 int screen_w,
2520 int screen_h,
2521 int player_mode,
2522 N_ISO_OBJECT* objects,
2523 int num_objects) {
2524 __n_assert(map, return);
2525 __n_assert(tile_bitmaps, return);
2526
2527 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2528
2529 float hw0 = map->proj.half_w;
2530 float hh0 = map->proj.half_h;
2531 float tl0 = map->proj.tile_lift;
2532 float phw = hw0 * zoom;
2533 float phh = hh0 * zoom;
2534 float lift = tl0 * zoom;
2535 float tile_draw_w = 2.0f * phw;
2536 float tile_draw_h = 2.0f * phh;
2537
2538 /* cam_px / cam_py are pre-computed pixel offsets provided by the caller.
2539 * The caller is responsible for integer-snapping the base camera offset
2540 * (floorf(cam_x * zoom)) and adding any chunk pixel offset after the
2541 * snap. This ensures all chunks share the same base snap and adjacent
2542 * chunk seams are pixel-perfect. */
2543
2544 int tile_w = tile_bitmaps[0] ? al_get_bitmap_width(tile_bitmaps[0]) : 64;
2545 int tile_h = tile_bitmaps[0] ? al_get_bitmap_height(tile_bitmaps[0]) : 32;
2546
2547 int num_edge_masks = ISO_NUM_EDGE_MASKS;
2548
2549 /* Build segment draw order */
2550 int max_draw_entries = map->width * map->height * ISO_MAX_SEGMENTS_PER_TILE;
2551 ISO_DRAW_ENTRY* draw_order = (ISO_DRAW_ENTRY*)malloc((size_t)max_draw_entries * sizeof(ISO_DRAW_ENTRY));
2552 if (!draw_order) return;
2553 int num_entries = iso_map_build_draw_order(map, draw_order, max_draw_entries);
2554
2555 /* Pre-process objects: compute screen positions and sort by depth.
2556 * Sort key matches segment draw order: (floor(fz), my, mx). */
2557 int obj_order_buf[64];
2558 int* obj_order = NULL;
2559 float* obj_sx_arr = NULL;
2560 float* obj_sy_arr = NULL;
2561 int nobj = (objects && num_objects > 0) ? num_objects : 0;
2562
2563 if (nobj > 0) {
2564 obj_order = (nobj <= 64) ? obj_order_buf : (int*)malloc((size_t)nobj * sizeof(int));
2565 obj_sx_arr = (float*)malloc((size_t)nobj * sizeof(float));
2566 obj_sy_arr = (float*)malloc((size_t)nobj * sizeof(float));
2567
2568 if (!obj_order || !obj_sx_arr || !obj_sy_arr) {
2569 n_log(LOG_ERR, "iso_map_draw: object allocation failed");
2570 if (obj_order && obj_order != obj_order_buf) free(obj_order);
2571 if (obj_sx_arr) free(obj_sx_arr);
2572 if (obj_sy_arr) free(obj_sy_arr);
2573 nobj = 0;
2574 obj_order = NULL;
2575 obj_sx_arr = NULL;
2576 obj_sy_arr = NULL;
2577 }
2578 }
2579
2580 if (nobj > 0) {
2581 for (int i = 0; i < nobj; i++) {
2582 obj_order[i] = i;
2583 objects[i].is_occluded = 0;
2584 objects[i].occlude_clip_y = 1e9f;
2585 float wsx, wsy;
2586 iso_map_to_screen_f(map, objects[i].fx, objects[i].fy, objects[i].fz, &wsx, &wsy);
2587 obj_sx_arr[i] = wsx * zoom + cam_px;
2588 obj_sy_arr[i] = wsy * zoom + cam_py;
2589 }
2590
2591 /* insertion sort by (depth, floor(fz), my, mx) to match segment draw order */
2592 for (int i = 1; i < nobj; i++) {
2593 int key = obj_order[i];
2594 int key_fz = (int)floorf(objects[key].fz);
2595 int key_my = (int)floorf(objects[key].fy);
2596 int key_mx = (int)floorf(objects[key].fx);
2597 int key_depth = key_my + key_mx;
2598 int j = i - 1;
2599 while (j >= 0) {
2600 int cj = obj_order[j];
2601 int cj_fz = (int)floorf(objects[cj].fz);
2602 int cj_my = (int)floorf(objects[cj].fy);
2603 int cj_mx = (int)floorf(objects[cj].fx);
2604 int cj_depth = cj_my + cj_mx;
2605 if (cj_depth > key_depth ||
2606 (cj_depth == key_depth && cj_fz > key_fz) ||
2607 (cj_depth == key_depth && cj_fz == key_fz && cj_my > key_my) ||
2608 (cj_depth == key_depth && cj_fz == key_fz && cj_my == key_my && cj_mx > key_mx)) {
2609 obj_order[j + 1] = obj_order[j];
2610 j--;
2611 } else {
2612 break;
2613 }
2614 }
2615 obj_order[j + 1] = key;
2616 }
2617 }
2618 /* ===== PASS 1: Ground segments only (bottom == 0), no objects =====
2619 * Draw all ground-level tiles first so they can never overdraw
2620 * elevated content (entities on bridges, floating segments, etc.). */
2621 for (int di = 0; di < num_entries; di++) {
2622 if (draw_order[di].bottom > 0) continue; /* skip elevated */
2623
2624 int mx = draw_order[di].mx;
2625 int my = draw_order[di].my;
2626 int seg_top = draw_order[di].top;
2627 int h = seg_top;
2628 int base = iso_map_get_terrain(map, mx, my);
2629
2630 /* Per-segment tile overrides: upper_tile for top face, lower_tile for walls */
2631 int top_terrain = base;
2632 int wall_terrain = base;
2633 {
2634 int si = draw_order[di].seg_idx;
2635 int tidx = my * map->width + mx;
2636 if (map->segments && tidx >= 0 && tidx < map->width * map->height &&
2637 si >= 0 && si < map->segments[tidx].count) {
2638 int ut = map->segments[tidx].segs[si].upper_tile;
2639 int lt = map->segments[tidx].segs[si].lower_tile;
2640 if (ut >= 0 && ut < map->num_terrains) top_terrain = ut;
2641 if (lt >= 0 && lt < map->num_terrains) wall_terrain = lt;
2642 }
2643 }
2644
2645 float vx[4], vy[4];
2646 float base_dx, base_dy;
2647
2648 if (map->smooth_height) {
2649 float h_n, h_e, h_s, h_w;
2650 iso_map_smooth_corner_heights(map, mx, my, &h_n, &h_e, &h_s, &h_w);
2651 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, h_n, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2652 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, h_e, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2653 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, h_s, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2654 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, h_w, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2655 base_dx = vx[3];
2656 base_dy = vy[0];
2657 } else {
2658 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, (float)h, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2659 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2660 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2661 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2662 base_dx = vx[3];
2663 base_dy = vy[0];
2664 }
2665
2666 /* Culling */
2667 float min_vy = fminf(fminf(vy[0], vy[1]), fminf(vy[2], vy[3]));
2668 float max_vy = fmaxf(fmaxf(vy[0], vy[1]), fmaxf(vy[2], vy[3]));
2669 float max_side = fmaxf((float)h, (float)map->max_height) * lift;
2670 if (vx[1] < 0 || vx[3] > (float)screen_w) continue;
2671 if (max_vy + max_side < 0 || min_vy > (float)screen_h) continue;
2672
2673 /* Height-based tint + ambient + dynamic lighting for this segment */
2674 float amb_r = map->ambient_r > 0.0f ? map->ambient_r : 1.0f;
2675 float amb_g = map->ambient_g > 0.0f ? map->ambient_g : 1.0f;
2676 float amb_b = map->ambient_b > 0.0f ? map->ambient_b : 1.0f;
2677 float dyn_r = 0.0f, dyn_g = 0.0f, dyn_b = 0.0f;
2678 if (map->dynamic_light_map) {
2679 int lidx = (my * map->width + mx) * 3;
2680 dyn_r = map->dynamic_light_map[lidx];
2681 dyn_g = map->dynamic_light_map[lidx + 1];
2682 dyn_b = map->dynamic_light_map[lidx + 2];
2683 }
2684 ALLEGRO_COLOR seg_tint = _compute_tile_tint(seg_top, map->max_height,
2686 amb_r, amb_g, amb_b,
2687 dyn_r, dyn_g, dyn_b);
2688 float seg_brightness = 1.0f;
2689 if (map->height_tint_intensity > 0.0f && map->max_height > 0) {
2690 float ratio = (float)seg_top / (float)map->max_height;
2691 seg_brightness = 1.0f - map->height_tint_intensity * (1.0f - ratio);
2692 }
2693 /* Factor in ambient for wall brightness */
2694 seg_brightness *= (amb_r + amb_g + amb_b) / 3.0f;
2695 float wall_dyn = (dyn_r + dyn_g + dyn_b) / 3.0f;
2696 seg_brightness += wall_dyn;
2697 if (seg_brightness > 1.0f) seg_brightness = 1.0f;
2698
2699 /* Top surface */
2700 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2701 if (map->smooth_height) {
2702 _draw_tile_warped(tile_bitmaps[top_terrain], vx, vy, tile_w, tile_h, seg_tint);
2703 } else {
2704 al_draw_tinted_scaled_bitmap(tile_bitmaps[top_terrain], seg_tint,
2705 0, 0, (float)tile_w, (float)tile_h,
2706 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2707 }
2708
2709 /* Cliff walls */
2710 if (seg_top > 0) {
2711 if (map->smooth_height) {
2712 int h_east = iso_map_get_height(map, mx + 1, my);
2713 int h_south = iso_map_get_height(map, mx, my + 1);
2714 int h_se = iso_map_get_height(map, mx + 1, my + 1);
2715 if ((h - h_east > map->smooth_slope_max) ||
2716 (h - h_south > map->smooth_slope_max) ||
2717 (h - h_se > map->smooth_slope_max)) {
2718 float ce_x, ce_y, cs_x, cs_y, cw_x, cw_y;
2719 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &ce_x, &ce_y);
2720 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &cs_x, &cs_y);
2721 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &cw_x, &cw_y);
2722 _draw_segment_sides(map, ce_x, ce_y, cs_x, cs_y, cw_x, cw_y,
2723 mx, my, 0, seg_top, lift, seg_brightness, wall_terrain);
2724 }
2725 } else {
2726 _draw_segment_sides(map, vx[1], vy[1], vx[2], vy[2], vx[3], vy[3],
2727 mx, my, 0, seg_top, lift, seg_brightness, wall_terrain);
2728 }
2729 }
2730
2731 /* Terrain transitions (same tint as base tile) */
2732 if (transition_tiles) {
2733 int edge_bits_arr[16];
2734 int corner_bits_arr[16];
2735 int nt = map->num_terrains < 16 ? map->num_terrains : 16;
2736 memset(edge_bits_arr, 0, sizeof(int) * (size_t)nt);
2737 memset(corner_bits_arr, 0, sizeof(int) * (size_t)nt);
2738 iso_map_calc_transitions_full(map, mx, my, edge_bits_arr, corner_bits_arr);
2739
2740 for (int t = base + 1; t < nt; t++) {
2741 if (edge_bits_arr[t] != 0 && transition_tiles[t] &&
2742 transition_tiles[t][edge_bits_arr[t]] != NULL) {
2743 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2744 if (map->smooth_height) {
2745 _draw_tile_warped(transition_tiles[t][edge_bits_arr[t]],
2746 vx, vy, tile_w, tile_h, seg_tint);
2747 } else {
2748 al_draw_tinted_scaled_bitmap(transition_tiles[t][edge_bits_arr[t]],
2749 seg_tint,
2750 0, 0, (float)tile_w, (float)tile_h,
2751 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2752 }
2753 }
2754
2755 int cidx = num_edge_masks + corner_bits_arr[t];
2756 if (corner_bits_arr[t] != 0 && cidx < num_masks &&
2757 transition_tiles[t] && transition_tiles[t][cidx] != NULL) {
2758 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2759 if (map->smooth_height) {
2761 vx, vy, tile_w, tile_h, seg_tint);
2762 } else {
2763 al_draw_tinted_scaled_bitmap(transition_tiles[t][cidx],
2764 seg_tint,
2765 0, 0, (float)tile_w, (float)tile_h,
2766 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2767 }
2768 }
2769 }
2770 }
2771
2772 /* Overlay tile (drawn after terrain transitions, before walls/borders) */
2773 if (overlay_bitmaps && map->overlay && num_overlay_tiles > 0) {
2774 int ov_idx = my * map->width + mx;
2775 int ov_tile = map->overlay[ov_idx];
2776 if (ov_tile > 0 && ov_tile < num_overlay_tiles && overlay_bitmaps[ov_tile]) {
2777 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2778 if (map->smooth_height) {
2779 _draw_tile_warped(overlay_bitmaps[ov_tile], vx, vy, tile_w, tile_h, seg_tint);
2780 } else {
2781 al_draw_tinted_scaled_bitmap(overlay_bitmaps[ov_tile], seg_tint,
2782 0, 0, (float)tile_w, (float)tile_h,
2783 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2784 }
2785 }
2786 }
2787
2788 /* Height borders */
2789 if (seg_top > 0) {
2790 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2791 _draw_height_borders(map, mx, my, draw_order[di].seg_idx,
2792 0, seg_top,
2793 draw_order[di].underside,
2794 vx, vy, lift);
2795 }
2796
2797 /* Hover highlight */
2798 if (mx == map->hover_mx && my == map->hover_my) {
2799 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2800 ALLEGRO_COLOR hc = player_mode
2801 ? al_map_rgba(100, 255, 100, 120)
2802 : al_map_rgba(255, 255, 100, 120);
2803 float verts[8] = {vx[0], vy[0], vx[1], vy[1],
2804 vx[2], vy[2], vx[3], vy[3]};
2805 al_draw_filled_polygon(verts, 4, hc);
2806 ALLEGRO_COLOR oc = player_mode
2807 ? al_map_rgba(100, 255, 100, 200)
2808 : al_map_rgba(255, 255, 100, 200);
2809 al_draw_line(vx[0], vy[0], vx[1], vy[1], oc, 1.5f);
2810 al_draw_line(vx[1], vy[1], vx[2], vy[2], oc, 1.5f);
2811 al_draw_line(vx[2], vy[2], vx[3], vy[3], oc, 1.5f);
2812 al_draw_line(vx[3], vy[3], vx[0], vy[0], oc, 1.5f);
2813 }
2814
2815 /* Grid overlay */
2816 if (map->show_grid) {
2817 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2818 ALLEGRO_COLOR gc = al_map_rgba(255, 255, 255, 60);
2819 al_draw_line(vx[0], vy[0], vx[1], vy[1], gc, 1.0f);
2820 al_draw_line(vx[1], vy[1], vx[2], vy[2], gc, 1.0f);
2821 al_draw_line(vx[2], vy[2], vx[3], vy[3], gc, 1.0f);
2822 al_draw_line(vx[3], vy[3], vx[0], vy[0], gc, 1.0f);
2823
2824 if (h > 0) {
2825 ALLEGRO_COLOR hc2 = al_map_rgba(255, 200, 100,
2826 (unsigned char)(60 + h * 35));
2827 float dot_x = (vx[0] + vx[2]) / 2.0f;
2828 float dot_y = (vy[0] + vy[1] + vy[2] + vy[3]) / 4.0f;
2829 al_draw_filled_circle(dot_x, dot_y, 2.0f * zoom, hc2);
2830 }
2831 }
2832 }
2833
2834 /* ===== PASS 2: Elevated segments + ALL objects (interleaved) =====
2835 * Objects are interleaved at every draw_order entry (ground and
2836 * elevated) to preserve correct depth ordering. Only elevated
2837 * segments (bottom > 0) are actually rendered — ground segments
2838 * were already drawn in Pass 1. */
2839 int obj_cursor = 0;
2840 for (int di = 0; di < num_entries; di++) {
2841 int mx = draw_order[di].mx;
2842 int my = draw_order[di].my;
2843 int seg_bottom = draw_order[di].bottom;
2844
2845 /* Object interleaving runs at every entry regardless of
2846 * ground/elevated — this ensures objects at ground-level depth
2847 * positions are drawn at the correct point in the sort order. */
2848 if (nobj > 0) {
2849 int seg_depth = my + mx;
2850 while (obj_cursor < nobj) {
2851 int oi = obj_order[obj_cursor];
2852 int obj_fz = (int)floorf(objects[oi].fz);
2853 int obj_my = (int)floorf(objects[oi].fy);
2854 int obj_mx = (int)floorf(objects[oi].fx);
2855 int obj_depth = obj_my + obj_mx;
2856 if (obj_depth > seg_depth ||
2857 (obj_depth == seg_depth && obj_fz > seg_bottom) ||
2858 (obj_depth == seg_depth && obj_fz == seg_bottom && obj_my > my) ||
2859 (obj_depth == seg_depth && obj_fz == seg_bottom && obj_my == my && obj_mx >= mx))
2860 break;
2861 if (objects[oi].draw) {
2862 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
2863 zoom, 1.0f, objects[oi].user_data);
2864 }
2865 obj_cursor++;
2866 }
2867 }
2868
2869 /* Ground segments already drawn in Pass 1 — skip rendering */
2870 if (seg_bottom == 0) continue;
2871
2872 /* Elevated segment rendering */
2873 int seg_top = draw_order[di].top;
2874 int h = seg_top;
2875 int base = iso_map_get_terrain(map, mx, my);
2876
2877 /* Per-segment tile overrides for elevated segments */
2878 int etop_terrain = base;
2879 int ewall_terrain = base;
2880 {
2881 int si = draw_order[di].seg_idx;
2882 int tidx = my * map->width + mx;
2883 if (map->segments && tidx >= 0 && tidx < map->width * map->height &&
2884 si >= 0 && si < map->segments[tidx].count) {
2885 int ut = map->segments[tidx].segs[si].upper_tile;
2886 int lt = map->segments[tidx].segs[si].lower_tile;
2887 if (ut >= 0 && ut < map->num_terrains) etop_terrain = ut;
2888 if (lt >= 0 && lt < map->num_terrains) ewall_terrain = lt;
2889 }
2890 }
2891
2892 float vx[4], vy[4];
2893 float base_dx, base_dy;
2894 _iso_corner_to_screen(hw0, hh0, tl0, mx, my, (float)h, cam_px, cam_py, zoom, &vx[0], &vy[0]);
2895 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my, (float)h, cam_px, cam_py, zoom, &vx[1], &vy[1]);
2896 _iso_corner_to_screen(hw0, hh0, tl0, mx + 1, my + 1, (float)h, cam_px, cam_py, zoom, &vx[2], &vy[2]);
2897 _iso_corner_to_screen(hw0, hh0, tl0, mx, my + 1, (float)h, cam_px, cam_py, zoom, &vx[3], &vy[3]);
2898 base_dx = vx[3];
2899 base_dy = vy[0];
2900
2901 /* Culling — objects already handled above, safe to continue */
2902 float min_vy = fminf(fminf(vy[0], vy[1]), fminf(vy[2], vy[3]));
2903 float max_vy = fmaxf(fmaxf(vy[0], vy[1]), fmaxf(vy[2], vy[3]));
2904 float max_side = fmaxf((float)h, (float)map->max_height) * lift;
2905 if (vx[1] < 0 || vx[3] > (float)screen_w) continue;
2906 if (max_vy + max_side < 0 || min_vy > (float)screen_h) continue;
2907
2908 /* Underside face for floating segments */
2909 if (draw_order[di].underside && tile_bitmaps[etop_terrain]) {
2910 float under_sx, under_sy;
2911 iso_map_to_screen(map, mx, my, seg_bottom, &under_sx, &under_sy);
2912 float under_dx = under_sx * zoom + cam_px;
2913 float under_dy = under_sy * zoom + cam_py;
2914 _draw_segment_underside(tile_bitmaps[etop_terrain], under_dx, under_dy,
2915 phw, phh, tile_draw_w, tile_draw_h,
2916 tile_w, tile_h);
2917 }
2918
2919 /* Height-based tint for elevated segment */
2920 /* Elevated segment: same ambient + dynamic light from base tile */
2921 float eamb_r = map->ambient_r > 0.0f ? map->ambient_r : 1.0f;
2922 float eamb_g = map->ambient_g > 0.0f ? map->ambient_g : 1.0f;
2923 float eamb_b = map->ambient_b > 0.0f ? map->ambient_b : 1.0f;
2924 float edyn_r = 0.0f, edyn_g = 0.0f, edyn_b = 0.0f;
2925 if (map->dynamic_light_map) {
2926 int elidx = (my * map->width + mx) * 3;
2927 edyn_r = map->dynamic_light_map[elidx];
2928 edyn_g = map->dynamic_light_map[elidx + 1];
2929 edyn_b = map->dynamic_light_map[elidx + 2];
2930 }
2931 ALLEGRO_COLOR eseg_tint = _compute_tile_tint(seg_top, map->max_height,
2933 eamb_r, eamb_g, eamb_b,
2934 edyn_r, edyn_g, edyn_b);
2935 float eseg_brightness = 1.0f;
2936 if (map->height_tint_intensity > 0.0f && map->max_height > 0) {
2937 float ratio = (float)seg_top / (float)map->max_height;
2938 eseg_brightness = 1.0f - map->height_tint_intensity * (1.0f - ratio);
2939 }
2940 eseg_brightness *= (eamb_r + eamb_g + eamb_b) / 3.0f;
2941 eseg_brightness += (edyn_r + edyn_g + edyn_b) / 3.0f;
2942 if (eseg_brightness > 1.0f) eseg_brightness = 1.0f;
2943
2944 /* Top surface (flat — elevated segments don't use smooth height) */
2945 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_INVERSE_ALPHA);
2946 al_draw_tinted_scaled_bitmap(tile_bitmaps[etop_terrain], eseg_tint,
2947 0, 0, (float)tile_w, (float)tile_h,
2948 base_dx, base_dy, tile_draw_w, tile_draw_h, 0);
2949
2950 /* Cliff walls + height borders */
2951 if (seg_top > seg_bottom) {
2952 _draw_segment_sides(map, vx[1], vy[1], vx[2], vy[2], vx[3], vy[3],
2953 mx, my, seg_bottom, seg_top, lift, eseg_brightness, ewall_terrain);
2954 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2955 _draw_height_borders(map, mx, my, draw_order[di].seg_idx,
2956 seg_bottom, seg_top,
2957 draw_order[di].underside,
2958 vx, vy, lift);
2959 }
2960
2961 /* Hover highlight */
2962 if (mx == map->hover_mx && my == map->hover_my) {
2963 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2964 ALLEGRO_COLOR hc = player_mode
2965 ? al_map_rgba(100, 255, 100, 120)
2966 : al_map_rgba(255, 255, 100, 120);
2967 float verts[8] = {vx[0], vy[0], vx[1], vy[1],
2968 vx[2], vy[2], vx[3], vy[3]};
2969 al_draw_filled_polygon(verts, 4, hc);
2970 ALLEGRO_COLOR oc = player_mode
2971 ? al_map_rgba(100, 255, 100, 200)
2972 : al_map_rgba(255, 255, 100, 200);
2973 al_draw_line(vx[0], vy[0], vx[1], vy[1], oc, 1.5f);
2974 al_draw_line(vx[1], vy[1], vx[2], vy[2], oc, 1.5f);
2975 al_draw_line(vx[2], vy[2], vx[3], vy[3], oc, 1.5f);
2976 al_draw_line(vx[3], vy[3], vx[0], vy[0], oc, 1.5f);
2977 }
2978
2979 /* Grid overlay */
2980 if (map->show_grid) {
2981 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
2982 ALLEGRO_COLOR gc = al_map_rgba(255, 255, 255, 60);
2983 al_draw_line(vx[0], vy[0], vx[1], vy[1], gc, 1.0f);
2984 al_draw_line(vx[1], vy[1], vx[2], vy[2], gc, 1.0f);
2985 al_draw_line(vx[2], vy[2], vx[3], vy[3], gc, 1.0f);
2986 al_draw_line(vx[3], vy[3], vx[0], vy[0], gc, 1.0f);
2987
2988 if (h > 0) {
2989 ALLEGRO_COLOR hc2 = al_map_rgba(255, 200, 100,
2990 (unsigned char)(60 + h * 35));
2991 float dot_x = (vx[0] + vx[2]) / 2.0f;
2992 float dot_y = (vy[0] + vy[1] + vy[2] + vy[3]) / 4.0f;
2993 al_draw_filled_circle(dot_x, dot_y, 2.0f * zoom, hc2);
2994 }
2995 }
2996 }
2997
2998 /* Draw any remaining objects past the last entry */
2999 while (obj_cursor < nobj) {
3000 int oi = obj_order[obj_cursor];
3001 if (objects[oi].draw) {
3002 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
3003 zoom, 1.0f, objects[oi].user_data);
3004 }
3005 obj_cursor++;
3006 }
3007
3008 /* Step 7: Occlusion detection and clipped overlay pass.
3009 * For each occluded entity, restrict the ghost draw to the region
3010 * below the occluding tile's north vertex using a clipping rectangle. */
3011 for (int i = 0; i < nobj; i++) {
3012 int oi = obj_order[i];
3013 if (objects[oi].occluded_alpha <= 0.0f) continue;
3014 if (!objects[oi].draw) continue;
3015
3016 float clip_y = 1e9f;
3018 objects[oi].fx, objects[oi].fy, objects[oi].fz,
3019 obj_sx_arr[oi], obj_sy_arr[oi],
3020 objects[oi].sprite_h,
3021 objects[oi].sprite_half_w,
3022 cam_px, cam_py, zoom,
3023 &clip_y))
3024 continue;
3025
3026 objects[oi].is_occluded = 1;
3027 objects[oi].occlude_clip_y = clip_y;
3028
3029 /* Clip ghost overlay to below the occluder's top edge */
3030 int prev_cx, prev_cy, prev_cw, prev_ch;
3031 al_get_clipping_rectangle(&prev_cx, &prev_cy, &prev_cw, &prev_ch);
3032 int clip_top = (int)floorf(clip_y);
3033 if (clip_top < prev_cy) clip_top = prev_cy;
3034 al_set_clipping_rectangle(prev_cx, clip_top,
3035 prev_cw, prev_cy + prev_ch - clip_top);
3036 objects[oi].draw(obj_sx_arr[oi], obj_sy_arr[oi],
3037 zoom, objects[oi].occluded_alpha, objects[oi].user_data);
3038 al_set_clipping_rectangle(prev_cx, prev_cy, prev_cw, prev_ch);
3039 }
3040
3041 /* Clean up */
3042 free(draw_order);
3043 if (obj_order && obj_order != obj_order_buf) free(obj_order);
3044 if (obj_sx_arr) free(obj_sx_arr);
3045 if (obj_sy_arr) free(obj_sy_arr);
3046} /* iso_map_draw() */
3047
3048/*---------------------------------------------------------------------------
3049 * Transition mask & tile generation (Article 934)
3050 *-------------------------------------------------------------------------*/
3051
3059void iso_mask_tile_to_diamond(ALLEGRO_BITMAP* bmp, int tile_w, int tile_h) {
3060 __n_assert(bmp, return);
3061
3062 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(bmp,
3063 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_READWRITE);
3064 if (!lr) return;
3065
3066 for (int py = 0; py < tile_h; py++) {
3067 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3068 for (int px = 0; px < tile_w; px++) {
3069 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3070 row[px * 4 + 0] = 0;
3071 row[px * 4 + 1] = 0;
3072 row[px * 4 + 2] = 0;
3073 row[px * 4 + 3] = 0;
3074 }
3075 }
3076 }
3077 al_unlock_bitmap(bmp);
3078}
3079
3090void iso_generate_transition_masks(ALLEGRO_BITMAP** masks, int tile_w, int tile_h) {
3091 __n_assert(masks, return);
3092
3093 float cx = (float)tile_w / 2.0f;
3094 float cy = (float)tile_h / 2.0f;
3095
3096 ALLEGRO_STATE state;
3097 al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
3098
3099 /* --- Edge masks (0..15) --- */
3100 for (int mask = 0; mask < ISO_NUM_EDGE_MASKS; mask++) {
3101 masks[mask] = al_create_bitmap(tile_w, tile_h);
3102 al_set_target_bitmap(masks[mask]);
3103 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3104
3105 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(masks[mask],
3106 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_WRITEONLY);
3107 if (!lr) continue;
3108
3109 for (int py = 0; py < tile_h; py++) {
3110 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3111 for (int px = 0; px < tile_w; px++) {
3112 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3113 row[px * 4 + 0] = 0;
3114 row[px * 4 + 1] = 0;
3115 row[px * 4 + 2] = 0;
3116 row[px * 4 + 3] = 0;
3117 continue;
3118 }
3119
3120 float alpha = 0.0f;
3121 float nx = ((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
3122 float ny = ((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
3123 float gw = 0.65f;
3124
3125 if (mask & ISO_EDGE_W) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (-nx - ny) / gw)));
3126 if (mask & ISO_EDGE_N) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (nx - ny) / gw)));
3127 if (mask & ISO_EDGE_E) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (nx + ny) / gw)));
3128 if (mask & ISO_EDGE_S) alpha = fmaxf(alpha, fminf(1.0f, fmaxf(0.0f, (-nx + ny) / gw)));
3129
3130 if (alpha > 1.0f) alpha = 1.0f;
3131 alpha = alpha * alpha * (3.0f - 2.0f * alpha);
3132
3133 unsigned char a = (unsigned char)(alpha * 255.0f);
3134 row[px * 4 + 0] = a;
3135 row[px * 4 + 1] = a;
3136 row[px * 4 + 2] = a;
3137 row[px * 4 + 3] = a;
3138 }
3139 }
3140 al_unlock_bitmap(masks[mask]);
3141 }
3142
3143 /* --- Corner masks (16..31) ---
3144 * In isometric diamond projection, diagonal neighbors map to diamond tips:
3145 * NW (mx-1,my-1) = North tip (0, -1) in normalized coords
3146 * NE (mx+1,my-1) = East tip (1, 0)
3147 * SE (mx+1,my+1) = South tip (0, 1)
3148 * SW (mx-1,my+1) = West tip (-1, 0) */
3149 for (int mask = 0; mask < ISO_NUM_CORNER_MASKS; mask++) {
3150 int idx = ISO_NUM_EDGE_MASKS + mask;
3151 masks[idx] = al_create_bitmap(tile_w, tile_h);
3152 al_set_target_bitmap(masks[idx]);
3153 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3154
3155 ALLEGRO_LOCKED_REGION* lr = al_lock_bitmap(masks[idx],
3156 ALLEGRO_PIXEL_FORMAT_ABGR_8888_LE, ALLEGRO_LOCK_WRITEONLY);
3157 if (!lr) continue;
3158
3159 for (int py = 0; py < tile_h; py++) {
3160 unsigned char* row = (unsigned char*)lr->data + py * lr->pitch;
3161 for (int px = 0; px < tile_w; px++) {
3162 if (!iso_is_in_diamond(px, py, tile_w, tile_h)) {
3163 row[px * 4 + 0] = 0;
3164 row[px * 4 + 1] = 0;
3165 row[px * 4 + 2] = 0;
3166 row[px * 4 + 3] = 0;
3167 continue;
3168 }
3169
3170 float alpha = 0.0f;
3171 float nx = ((float)px + 0.5f - cx) / ((float)tile_w / 2.0f);
3172 float ny = ((float)py + 0.5f - cy) / ((float)tile_h / 2.0f);
3173 float corner_radius = 0.55f;
3174
3175 if (mask & ISO_CORNER_NW) {
3176 float dy = ny + 1.0f;
3177 float dist = sqrtf(nx * nx + dy * dy);
3178 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3179 alpha = fmaxf(alpha, c_alpha);
3180 }
3181 if (mask & ISO_CORNER_NE) {
3182 float dx = nx - 1.0f;
3183 float dist = sqrtf(dx * dx + ny * ny);
3184 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3185 alpha = fmaxf(alpha, c_alpha);
3186 }
3187 if (mask & ISO_CORNER_SE) {
3188 float dy = ny - 1.0f;
3189 float dist = sqrtf(nx * nx + dy * dy);
3190 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3191 alpha = fmaxf(alpha, c_alpha);
3192 }
3193 if (mask & ISO_CORNER_SW) {
3194 float dx = nx + 1.0f;
3195 float dist = sqrtf(dx * dx + ny * ny);
3196 float c_alpha = fmaxf(0.0f, 1.0f - dist / corner_radius);
3197 alpha = fmaxf(alpha, c_alpha);
3198 }
3199
3200 if (alpha > 1.0f) alpha = 1.0f;
3201 alpha = alpha * alpha * (3.0f - 2.0f * alpha);
3202
3203 unsigned char a = (unsigned char)(alpha * 255.0f);
3204 row[px * 4 + 0] = a;
3205 row[px * 4 + 1] = a;
3206 row[px * 4 + 2] = a;
3207 row[px * 4 + 3] = a;
3208 }
3209 }
3210 al_unlock_bitmap(masks[idx]);
3211 }
3212
3213 al_restore_state(&state);
3214 n_log(LOG_INFO, "Generated %d transition masks (%dx%d)", ISO_NUM_MASKS, tile_w, tile_h);
3215}
3216
3221void iso_generate_transition_tiles(ALLEGRO_BITMAP*** tiles,
3222 ALLEGRO_BITMAP** masks,
3223 ALLEGRO_BITMAP** tile_bitmaps,
3224 int num_terrains,
3225 int tile_w,
3226 int tile_h) {
3227 __n_assert(tiles, return);
3228 __n_assert(masks, return);
3229 __n_assert(tile_bitmaps, return);
3230
3231 ALLEGRO_STATE state;
3232 al_store_state(&state, ALLEGRO_STATE_TARGET_BITMAP | ALLEGRO_STATE_BLENDER);
3233
3234 for (int t = 0; t < num_terrains; t++) {
3235 for (int m = 0; m < ISO_NUM_MASKS; m++) {
3236 /* Skip mask 0 (no edges active) and mask 16 (no corners active) */
3237 if ((m < ISO_NUM_EDGE_MASKS && m == 0) ||
3238 (m >= ISO_NUM_EDGE_MASKS && m == ISO_NUM_EDGE_MASKS)) {
3239 tiles[t][m] = NULL;
3240 continue;
3241 }
3242
3243 tiles[t][m] = al_create_bitmap(tile_w, tile_h);
3244 al_set_target_bitmap(tiles[t][m]);
3245 al_clear_to_color(al_map_rgba(0, 0, 0, 0));
3246
3247 /* Draw the terrain tile first */
3248 al_set_blender(ALLEGRO_ADD, ALLEGRO_ONE, ALLEGRO_ZERO);
3249 al_draw_bitmap(tile_bitmaps[t], 0, 0, 0);
3250
3251 /* Multiply by the alpha mask: keeps terrain color
3252 * but applies the mask's alpha for the transition gradient */
3253 al_set_blender(ALLEGRO_ADD, ALLEGRO_DEST_COLOR, ALLEGRO_ZERO);
3254 al_draw_bitmap(masks[m], 0, 0, 0);
3255 }
3256 }
3257
3258 al_restore_state(&state);
3259 n_log(LOG_INFO, "Pre-composited transition tiles for %d terrains x %d masks", num_terrains, ISO_NUM_MASKS);
3260}
3261
3262#endif /* HAVE_ALLEGRO - iso_map_draw */
3263
3264#ifdef N_ASTAR_H
3274ASTAR_GRID* iso_map_to_astar_grid(const ISO_MAP* map, int max_height_diff, int start_x, int start_y) {
3275 __n_assert(map, return NULL);
3276
3277 ASTAR_GRID* grid = n_astar_grid_new(map->width, map->height, 1);
3278 if (!grid) return NULL;
3279
3280 int total = map->width * map->height;
3281
3282 /* First pass: all cells unwalkable, set costs */
3283 for (int y = 0; y < map->height; y++) {
3284 for (int x = 0; x < map->width; x++) {
3285 n_astar_grid_set_walkable(grid, x, y, 0, 0);
3286 int h = map->heightmap[y * map->width + x];
3287 int cost = ASTAR_COST_CARDINAL + h * 100;
3288 n_astar_grid_set_cost(grid, x, y, 0, cost);
3289 }
3290 }
3291
3292 /* Flood-fill from (start_x, start_y) to mark reachable cells as walkable.
3293 * This ensures height constraints are checked per-edge (between consecutive
3294 * cells) rather than per-cell, so tiles on a plateau edge remain walkable
3295 * when approached from the same elevation. */
3296 if (start_x < 0 || start_x >= map->width || start_y < 0 || start_y >= map->height)
3297 return grid;
3298
3299 int start_ab = map->ability[start_y * map->width + start_x];
3300 if (start_ab != WALK && start_ab != SWIM)
3301 return grid;
3302
3303 int* queue = (int*)malloc((size_t)total * 2 * sizeof(int));
3304 if (!queue) {
3305 n_astar_grid_free(grid);
3306 return NULL;
3307 }
3308
3309 uint8_t* visited = (uint8_t*)calloc((size_t)total, sizeof(uint8_t));
3310 if (!visited) {
3311 free(queue);
3312 n_astar_grid_free(grid);
3313 return NULL;
3314 }
3315
3316 int qfront = 0, qback = 0;
3317
3318 /* seed the start cell */
3319 visited[start_y * map->width + start_x] = 1;
3320 n_astar_grid_set_walkable(grid, start_x, start_y, 0, 1);
3321 queue[qback++] = start_x;
3322 queue[qback++] = start_y;
3323
3324 /* 8-directional expansion (matches ASTAR_ALLOW_DIAGONAL) */
3325 const int dirs[][2] = {{-1, 0}, {1, 0}, {0, -1}, {0, 1}, {-1, -1}, {-1, 1}, {1, -1}, {1, 1}};
3326
3327 while (qfront < qback) {
3328 int cx = queue[qfront++];
3329 int cy = queue[qfront++];
3330 int ch = map->heightmap[cy * map->width + cx];
3331
3332 for (int d = 0; d < 8; d++) {
3333 int nx = cx + dirs[d][0];
3334 int ny = cy + dirs[d][1];
3335 if (nx < 0 || nx >= map->width || ny < 0 || ny >= map->height) continue;
3336 if (visited[ny * map->width + nx]) continue;
3337
3338 int nab = map->ability[ny * map->width + nx];
3339 if (nab != WALK && nab != SWIM) continue;
3340
3341 int nh = map->heightmap[ny * map->width + nx];
3342 if (max_height_diff >= 0 && abs(ch - nh) > max_height_diff) continue;
3343
3344 visited[ny * map->width + nx] = 1;
3345 n_astar_grid_set_walkable(grid, nx, ny, 0, 1);
3346 queue[qback++] = nx;
3347 queue[qback++] = ny;
3348 }
3349 }
3350
3351 free(queue);
3352 free(visited);
3353
3354 return grid;
3355}
3356#endif /* N_ASTAR_H */
3357
3358#endif /* #ifndef NOISOENGINE */
static int player_mode
Definition ex_gui.c:54
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:296
void n_astar_grid_free(ASTAR_GRID *grid)
Free a grid and all its internal data.
Definition n_astar.c:255
#define ASTAR_COST_CARDINAL
Default cost for straight movement (fixed-point x1000)
Definition n_astar.h:80
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:270
ASTAR_GRID * n_astar_grid_new(int width, int height, int depth)
Create a new grid for A* pathfinding.
Definition n_astar.c:219
Grid structure holding walkability, costs, and dimensions.
Definition n_astar.h:147
#define Malloc(__ptr, __struct, __size)
Malloc Handler to get errors and set to 0.
Definition n_common.h:203
#define __n_assert(__ptr, __ret)
macro to assert things
Definition n_common.h:278
#define Free(__ptr)
Free Handler to get errors.
Definition n_common.h:262
int bottom
segment bottom height (primary sort key)
int X
X move in pixel for drawing.
int TILEW
size X of tiles of the map (in pixel)
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 ability
ability of the tile (walking, swimming, blocking, killing ?)
ALLEGRO_BITMAP * colortile
default full colored background tile
int upper_tile
terrain index for top face (-1 = use tile's terrain layer)
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
ALLEGRO_COLOR bgcolor
color of the bg
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 tilenumber
ident of the tile in the MAP->tile library
float half_w
half-width of a tile in pixels (horizontal extent)
int count
0..ISO_MAX_SEGMENTS_PER_TILE
char * name
Name of the map ( used for linking between two map )
int ptanchorY
Y starting cell for drawing.
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 ptanchorX
X starting cell for drawing.
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 TILEH
size Y of tiles of the map (in pixel)
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
CELL * grid
Grid of each cell of the map.
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_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
int YSIZE
size Y of the grid (nbYcell)
int XSIZE
size X of the grid (nbXcell)
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 music
ident of the music on the tile
int draw_se
1 if SE edge (E→S, bottom-right) should be drawn
int my
tile coordinates
int objectnumber
ident of the object in the MAP->object library
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
int Y
Y move in pixel for drawing.
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)
float x
camera X offset (world units, pre-zoom)
ISO_PROJECTION proj
current projection parameters
ALLEGRO_COLOR wirecolor
color of wire
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])
int save_map(MAP *map, char *filename)
Save the map to filename.
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.
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.
int get_value(MAP *map, int type, int x, int y)
Get a the tilenumber of a cell item.
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.
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.
int draw_map(MAP *map, ALLEGRO_BITMAP *bmp, int destx, int desty, int mode)
Draw a MAP *map on the ALLEGRO_BITMAP *bmp.
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.
#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.
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).
int load_map(MAP **map, char *filename)
Load the map from filename.
#define ISO_PROJ_STAGGERED
Projection ID: flatter dimetric (~18.43 degree angle)
int camera_to_map(MAP **map, int tx, int ty, int x, int y)
Center Map on given map coordinate, with x & y offset.
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.
#define N_OBJECT
FLAG of object type.
int iso_map_get_terrain(const ISO_MAP *map, int mx, int my)
Get the terrain type at a cell.
int camera_to_scr(MAP **map, int x, int y)
Center Map on given screen coordinate.
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.
int set_value(MAP *map, int type, int x, int y, int value)
Set the value of a cell in a map.
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).
#define N_MUSIC
FLAG of music type.
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.
int ScreenToMap(int mx, int my, int *Tilex, int *Tiley, ALLEGRO_BITMAP *mousemap)
Convert screen coordinate to map coordinate.
#define N_TILE
FLAG of tile type.
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_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 N_ABILITY
FLAG of ability type.
#define ISO_CORNER_NW
Corner bitmask: northwest diagonal has higher terrain.
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 n_iso_camera_scroll(N_ISO_CAMERA *cam, float dx, float dy)
Scroll the camera by (dx, dy) world units.
int free_map(MAP **map)
Free the map.
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)
int create_empty_map(MAP **map, const char *name, int XSIZE, int YSIZE, int TILEW, int TILEH, int nbmaxobjects, int nbmaxgroup, int nbmaxanims, int nbtiles, int nbanims)
Create an empty map.
Cell of a MAP.
Draw order entry for segment-sorted rendering.
Height-aware isometric map with terrain and height layers.
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.
MAP with objects, tiles, skins (legacy structure)
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:88
#define LOG_ERR
error conditions
Definition n_log.h:75
#define LOG_INFO
informational
Definition n_log.h:81
A* Pathfinding API for 2D and 3D grids.
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 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 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 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...