Nilorea Library
C utilities for networking, threading, graphics
Loading...
Searching...
No Matches
ex_gui_isometric.c
Go to the documentation of this file.
1/*
2 * Nilorea Library
3 * Copyright (C) 2005-2026 Castagnier Mickael
4 *
5 * Licensed under the Apache License, Version 2.0 (the "License");
6 * you may not use this file except in compliance with the License.
7 * You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14 * implied. See the License for the specific language governing
15 * permissions and limitations under the License.
16 *
17 * SPDX-License-Identifier: Apache-2.0
18 */
19
46
47#include <stdio.h>
48#include <stdlib.h>
49#include <string.h>
50#include <math.h>
51#include <time.h>
52#include <stdint.h>
53
54/* Allegro5 */
55#include <allegro5/allegro.h>
56#include <allegro5/allegro_image.h>
57#include <allegro5/allegro_primitives.h>
58#include <allegro5/allegro_font.h>
59#include <allegro5/allegro_ttf.h>
60
61/* nilorea-library modules */
62#include "nilorea/n_common.h"
63#include "nilorea/n_log.h"
64#include "nilorea/n_list.h"
65#include "nilorea/n_str.h"
66#include "nilorea/n_astar.h"
69#include "nilorea/n_gui.h"
70
71/* Constants */
72#define SCREEN_W 1280
73#define SCREEN_H 720
74#define FPS 60.0
75
76/* Tile dimensions (isometric diamond) */
77#define TILE_W 66
78#define TILE_H 34
79
80/* Map size */
81#define MAP_W 20
82#define MAP_H 20
83
84/* Number of terrain types */
85#define NUM_TERRAINS 8
86
87/* Height / elevation settings */
88#define MAX_HEIGHT 5
89#define TILE_LIFT 16
90
91/* Player movement settings */
92#define PLAYER_MOVE_SPEED 4.0f
93#define JUMP_VELOCITY 9.0f /* initial upward velocity (height units/sec) */
94#define JUMP_GRAVITY 20.0f /* gravity for jump (height units/sec^2) */
95#define CAM_SMOOTHING 8.0f /* camera lerp speed (higher = snappier) */
96#define PLAYER_MAX_HEIGH_DIFF 1 /* max height diff player can walk over */
97
98/* Terrain type IDs, ordered by visual precedence (higher = drawn on top) */
99#define TERRAIN_EAU 0 /* water - lowest precedence, base terrain */
100#define TERRAIN_LAVE 1 /* lava */
101#define TERRAIN_SABLE 2 /* sand */
102#define TERRAIN_CHEMIN 3 /* road */
103#define TERRAIN_TEMPLE3 4 /* temple floor light */
104#define TERRAIN_TEMPLE4 5 /* temple floor dark */
105#define TERRAIN_PAILLE 6 /* straw */
106#define TERRAIN_ROCAILLE 7 /* rock - highest precedence */
107
108/* Terrain names for HUD */
109static const char* terrain_names[NUM_TERRAINS] = {
110 "Water", "Lava", "Sand", "Road", "Temple Light", "Temple Dark", "Straw", "Rock"};
111
112/* Terrain file names */
113static const char* terrain_files[NUM_TERRAINS] = {
114 "DATAS/tiles/eau_centre.bmp",
115 "DATAS/tiles/lave.bmp",
116 "DATAS/tiles/sable_centre.bmp",
117 "DATAS/tiles/chemin_centre.bmp",
118 "DATAS/tiles/temple3.bmp",
119 "DATAS/tiles/temple4.bmp",
120 "DATAS/tiles/paille.bmp",
121 "DATAS/tiles/rocaille.bmp"};
122
123/* Projection presets, use library defines (ISO_PROJ_CLASSIC, etc.) */
124#define NUM_PROJECTIONS ISO_NUM_PROJECTIONS
125
126/* Transition mask count alias */
127#define NUM_MASKS ISO_NUM_MASKS
128
129/* State */
131
132static ISO_MAP* isomap = NULL;
133static N_ISO_CAMERA* camera = NULL;
134
135static ALLEGRO_BITMAP* tile_bitmaps[NUM_TERRAINS] = {NULL};
136static ALLEGRO_BITMAP* transition_masks[NUM_MASKS] = {NULL};
137static ALLEGRO_BITMAP* transition_tiles[NUM_TERRAINS][NUM_MASKS] = {{NULL}};
138
139/* Dynamic screen dimensions */
140static int screen_w = SCREEN_W;
141static int screen_h = SCREEN_H;
142
143/* UI state */
145static int paint_height = 0;
146static bool height_mode = false;
147static bool show_grid = false;
148static bool running = true;
149static bool redraw_needed = true;
150static bool smooth_height = false;
151static int smooth_slope_max = 1;
152
153/* Player state: free movement with continuous float position */
154static bool player_mode = false; /* true = player control, false = editor */
155static float player_fx = 10.5f; /* continuous map X (tile center = int + 0.5) */
156static float player_fy = 10.5f; /* continuous map Y */
157static int player_mx = 10; /* current tile X (derived) */
158static int player_my = 10; /* current tile Y (derived) */
159static float player_screen_x = 0.0f; /* derived screen position */
160static float player_screen_y = 0.0f;
161static float player_z = 0.0f; /* absolute height position (height units) */
162static float player_vz = 0.0f; /* vertical velocity (height units/sec) */
163static bool player_on_ground = true; /* true when touching the ground */
164
165/* Dead Reckoning ghost: simulated remote player on circular path */
166static bool ghost_enabled = false;
167static DR_ENTITY* ghost_dr = NULL;
168static float ghost_screen_x = 0.0f;
169static float ghost_screen_y = 0.0f;
170static float ghost_true_x = 0.0f;
171static float ghost_true_y = 0.0f;
172static float ghost_path_cx = 10.0f;
173static float ghost_path_cy = 10.0f;
174static float ghost_path_radius = 6.0f;
175static float ghost_path_speed = 0.4f;
176static float ghost_path_angle = 0.0f;
177static double ghost_last_send = 0.0;
178static double ghost_send_interval = 0.25;
179
180/* Mouse hover tile */
181static int hover_mx = -1;
182static int hover_my = -1;
183static int last_mouse_x = 0;
184static int last_mouse_y = 0;
185
186/* GUI context and widget IDs */
187static N_GUI_CTX* gui_ctx = NULL;
188static int gui_win_modes = -1;
189static int gui_win_tiles = -1;
190static int gui_win_proj = -1;
191static int gui_win_info = -1;
192static int gui_btn_player = -1;
193static int gui_btn_height = -1;
194static int gui_btn_grid = -1;
195static int gui_btn_smooth = -1;
196static int gui_btn_ghost = -1;
197static int gui_btn_reset = -1;
198static int gui_lbl_height = -1;
199static int gui_sld_height = -1;
200static int gui_lst_tiles = -1;
201static int gui_lbl_slope = -1;
202static int gui_sld_slope = -1;
203static int gui_btn_proj[NUM_PROJECTIONS] = {-1, -1, -1, -1};
204static int gui_lbl_status = -1;
205static int gui_lbl_proj = -1;
206static int gui_lbl_hover = -1;
207
208/* Ghost dead reckoning GUI */
209static int gui_win_ghost_dr = -1;
210static int gui_lbl_dr_algo = -1;
211static int gui_rad_dr_algo = -1;
212static int gui_lbl_dr_blend = -1;
213static int gui_rad_dr_blend = -1;
214static int gui_lbl_dr_interval = -1;
215static int gui_sld_dr_interval = -1;
216static int gui_lbl_dr_blend_time = -1;
217static int gui_sld_dr_blend_time = -1;
218static int gui_lbl_dr_threshold = -1;
219static int gui_sld_dr_threshold = -1;
220
221/* A* pathfinding for click-to-move */
222static ASTAR_PATH* player_path = NULL;
223static int player_path_idx = 0;
224static float player_path_progress = 0.0f;
225/* Exact fractional click destination within the final tile */
226static float player_click_fx = 0.0f;
227static float player_click_fy = 0.0f;
228
229#define MAP_FILE "iso_gui_map.isom"
230
231/* Diamond helpers, coordinate conversion, projection interpolation,
232 * transition mask/tile generation, and diamond masking are all provided
233 * by the nilorea-library n_iso_engine API. */
234
235/* Bilinear height interpolation with cliff-clamping */
236static float interpolate_height_at(float fx, float fy) {
237 if (fx < 0.0f) fx = 0.0f;
238 if (fy < 0.0f) fy = 0.0f;
239 if (fx > (float)MAP_W - 1e-4f) fx = (float)MAP_W - 1e-4f;
240 if (fy > (float)MAP_H - 1e-4f) fy = (float)MAP_H - 1e-4f;
241 return iso_map_interpolate_height(isomap, fx, fy);
242}
243
244/* Collision-aware height interpolation: clamp neighbor tile heights so the
245 * bilinear blend does not pull the player upward into wall tiles.
246 * The standard interpolation samples (ix,iy), (ix+1,iy), (ix,iy+1),
247 * (ix+1,iy+1). When one of those neighbors is a tall wall, the smooth
248 * blend makes the player "climb" the wall before the tile-boundary
249 * collision check fires. Clamping each neighbor to at most
250 * base_height + max_climb prevents that. */
251static float interpolate_height_at_clamped(float fx, float fy, int max_climb) {
252 if (fx < 0.0f) fx = 0.0f;
253 if (fy < 0.0f) fy = 0.0f;
254 if (fx > (float)MAP_W - 1e-4f) fx = (float)MAP_W - 1e-4f;
255 if (fy > (float)MAP_H - 1e-4f) fy = (float)MAP_H - 1e-4f;
256
257 int ix = (int)floorf(fx);
258 int iy = (int)floorf(fy);
259
260 float h00 = (float)iso_map_get_height(isomap, ix, iy);
261
262 /* In CUT mode, return flat tile height, no interpolation */
263 if (!smooth_height) {
264 return h00;
265 }
266
267 float frac_x = fx - (float)ix;
268 float frac_y = fy - (float)iy;
269
270 /* Clamp neighbor indices to valid range for edge tiles */
271 int ix1 = (ix + 1 < MAP_W) ? ix + 1 : ix;
272 int iy1 = (iy + 1 < MAP_H) ? iy + 1 : iy;
273
274 float h10 = (float)iso_map_get_height(isomap, ix1, iy);
275 float h01 = (float)iso_map_get_height(isomap, ix, iy1);
276 float h11 = (float)iso_map_get_height(isomap, ix1, iy1);
277
278 float max_h = h00 + (float)max_climb;
279 if (h10 > max_h) h10 = max_h;
280 if (h01 > max_h) h01 = max_h;
281 if (h11 > max_h) h11 = max_h;
282
283 float top = h00 + (h10 - h00) * frac_x;
284 float bot = h01 + (h11 - h01) * frac_x;
285 return top + (bot - top) * frac_y;
286}
287
288/* Object draw callbacks for N_ISO_OBJECT depth-sorted rendering */
289
290/* Player draw callback: green circle with shadow */
291static void draw_player_object(float sx, float sy, float zoom, float alpha, void* user_data) {
292 (void)user_data;
293 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
294 float cy = sy - 8.0f * zoom; /* offset up from feet */
295
296 /* Shadow at ground level (only for normal draw, not overlay) */
297 if (alpha >= 1.0f) {
299 float gs_sx, gs_sy;
300 iso_map_to_screen_f(isomap, player_fx, player_fy, ground_h, &gs_sx, &gs_sy);
301 float shadow_cx, shadow_cy;
302 n_iso_camera_world_to_screen(camera, gs_sx, gs_sy, &shadow_cx, &shadow_cy);
303 al_draw_filled_ellipse(sx, shadow_cy, 4.0f * zoom, 2.0f * zoom,
304 al_map_rgba(0, 0, 0, 60));
305 }
306
307 al_draw_filled_circle(sx, cy, 5.0f * zoom,
308 al_map_rgba(50, 200, 50, (unsigned char)(220.0f * alpha)));
309 al_draw_circle(sx, cy, 5.0f * zoom,
310 al_map_rgba(255, 255, 255, (unsigned char)(200.0f * alpha)), 1.5f);
311}
312
313/* Ghost draw callback: blue circle (DR position) + red outline (true position) */
314static void draw_ghost_object(float sx, float sy, float zoom, float alpha, void* user_data) {
315 (void)user_data;
316 al_set_blender(ALLEGRO_ADD, ALLEGRO_ALPHA, ALLEGRO_INVERSE_ALPHA);
317 float cy = sy - 8.0f * zoom;
318
319 /* DR-computed position (blue) */
320 al_draw_filled_circle(sx, cy, 5.0f * zoom,
321 al_map_rgba(80, 120, 255, (unsigned char)(220.0f * alpha)));
322 al_draw_circle(sx, cy, 5.0f * zoom,
323 al_map_rgba(200, 200, 255, (unsigned char)(200.0f * alpha)), 1.5f);
324 if (alpha >= 1.0f) {
325 al_draw_filled_ellipse(sx, cy + 8.0f * zoom, 4.0f * zoom, 2.0f * zoom,
326 al_map_rgba(0, 0, 0, 60));
327
328 /* True position: red outline (only in normal draw) */
329 float tcx, tcy;
331 tcy -= 8.0f * zoom;
332 al_draw_circle(tcx, tcy, 5.0f * zoom, al_map_rgba(255, 80, 80, 180), 1.5f);
333 al_draw_filled_ellipse(tcx, tcy + 8.0f * zoom, 4.0f * zoom, 2.0f * zoom,
334 al_map_rgba(255, 0, 0, 30));
335 }
336}
337
338/* Randomize the map */
339static void randomize_map(void) {
340 srand((unsigned)time(NULL));
341
342 for (int y = 0; y < MAP_H; y++)
343 for (int x = 0; x < MAP_W; x++) {
345 iso_map_set_height(isomap, x, y, 0);
346 }
347
348 for (int patch = 0; patch < 15; patch++) {
349 int t = rand() % NUM_TERRAINS;
350 int cx = rand() % MAP_W;
351 int cy = rand() % MAP_H;
352 int radius = 2 + rand() % 4;
353 for (int dy = -radius; dy <= radius; dy++)
354 for (int dx = -radius; dx <= radius; dx++) {
355 int x = cx + dx, y = cy + dy;
356 if (x < 0 || x >= MAP_W || y < 0 || y >= MAP_H) continue;
357 float dist = sqrtf((float)(dx * dx + dy * dy));
358 if (dist <= radius && (rand() % 100) < (int)(100.0f * (1.0f - dist / (float)(radius + 1))))
359 iso_map_set_terrain(isomap, x, y, t);
360 }
361 }
362
363 for (int hill = 0; hill < 8; hill++) {
364 int peak_h = 1 + rand() % MAX_HEIGHT;
365 int cx = rand() % MAP_W;
366 int cy = rand() % MAP_H;
367 int radius = 2 + rand() % 3;
368 for (int dy = -radius; dy <= radius; dy++)
369 for (int dx = -radius; dx <= radius; dx++) {
370 int x = cx + dx, y = cy + dy;
371 if (x < 0 || x >= MAP_W || y < 0 || y >= MAP_H) continue;
372 float dist = sqrtf((float)(dx * dx + dy * dy));
373 if (dist > radius) continue;
374 int h = (int)((float)peak_h * (1.0f - dist / (float)(radius + 1)) + 0.5f);
375 if (h > iso_map_get_height(isomap, x, y))
376 iso_map_set_height(isomap, x, y, h);
377 }
378 }
379}
380
381/* Button callbacks for n_gui_button_set_keycode bindings */
382
383/* Toggle player mode (P key) */
384static void on_player_toggle(int id, void* data) {
385 (void)id;
386 (void)data;
388 if (player_mode) {
389 if (height_mode) {
390 height_mode = false;
392 }
393 int drop_max_h = 0;
394 for (int y = 0; y < MAP_H; y++)
395 for (int x = 0; x < MAP_W; x++) {
396 int h = iso_map_get_height(isomap, x, y);
397 if (h > drop_max_h) drop_max_h = h;
398 }
399 player_z = (float)(drop_max_h + 1);
400 player_vz = 0.0f;
401 player_on_ground = false;
406 }
407}
408
409/* Toggle height editing mode (H key) */
410static void on_height_toggle(int id, void* data) {
411 (void)id;
412 (void)data;
414 if (height_mode && player_mode) {
415 player_mode = false;
417 }
418}
419
420/* Toggle grid overlay (G key) */
421static void on_grid_toggle(int id, void* data) {
422 (void)id;
423 (void)data;
425}
426
427/* Toggle smooth height (V key) */
428static void on_smooth_toggle(int id, void* data) {
429 (void)id;
430 (void)data;
432}
433
434/* Toggle ghost dead reckoning display (N key) */
435static void on_ghost_toggle(int id, void* data) {
436 (void)id;
437 (void)data;
439 if (ghost_enabled) {
441 if (ghost_dr) {
442 ghost_last_send = al_get_time();
443 ghost_path_angle = 0.0f;
444 int ghost_max_h = 0;
445 for (int y = 0; y < MAP_H; y++)
446 for (int x = 0; x < MAP_W; x++) {
447 int h = iso_map_get_height(isomap, x, y);
448 if (h > ghost_max_h) ghost_max_h = h;
449 }
451 ghost_path_cy, (double)(ghost_max_h + 1));
452 DR_VEC3 init_vel = dr_vec3(0.0, ghost_path_radius * ghost_path_speed, 0.0);
453 dr_entity_set_position(ghost_dr, &init_pos, &init_vel, NULL,
454 al_get_time());
455 }
456 } else {
458 }
459}
460
461/* reset terrain (R key) */
462static void on_reset_click(int id, void* data) {
463 (void)id;
464 (void)data;
467}
468
469/* Select projection (F1-F4 keys) */
470static void on_proj_select(int id, void* data) {
471 (void)data;
472 for (int p = 0; p < NUM_PROJECTIONS; p++) {
473 if (gui_btn_proj[p] == id) {
474 current_proj = p;
476 }
478 }
479}
480
481/* Main */
482int main(int argc, char* argv[]) {
483 (void)argc;
484 (void)argv;
485
487
488 if (!al_init()) {
489 n_abort("Could not init Allegro.\n");
490 }
491 /* The data is in DATAS/ beside the executable: found from wherever the
492 * example is started, not only from examples/. Started elsewhere it used
493 * to fail on its data with a window already open, and exiting that way
494 * crashed on some OpenGL drivers. */
495 {
496 ALLEGRO_PATH* here = al_get_standard_path(ALLEGRO_RESOURCES_PATH);
497
498 if (here) {
499 al_change_directory(al_path_cstr(here, ALLEGRO_NATIVE_PATH_SEP));
500 al_destroy_path(here);
501 }
502 }
503
504 /* Every tile is looked for before any window exists, so a missing one
505 * ends the example with nothing to release. */
506 for (int i = 0; i < NUM_TERRAINS; i++) {
507 if (!al_filename_exists(terrain_files[i])) {
508 n_log(LOG_ERR, "Missing tile %s beside the executable", terrain_files[i]);
509 return EXIT_FAILURE;
510 }
511 }
512 if (!al_init_image_addon()) {
513 n_abort("image addon\n");
514 }
515 if (!al_init_primitives_addon()) {
516 n_abort("primitives addon\n");
517 }
518 if (!al_init_font_addon()) {
519 n_abort("font addon\n");
520 }
521 al_init_ttf_addon();
522 al_install_keyboard();
523 al_install_mouse();
524
525 al_set_new_display_flags(ALLEGRO_OPENGL | ALLEGRO_WINDOWED | ALLEGRO_RESIZABLE);
526 ALLEGRO_DISPLAY* display = al_create_display(SCREEN_W, SCREEN_H);
527 if (!display) {
528 n_abort("Unable to create display\n");
529 }
530 al_set_window_title(display, "Nilorea Isometric Engine + GUI Demo");
531
532 ALLEGRO_FONT* font = al_create_builtin_font();
533 if (!font) {
534 n_abort("Unable to create builtin font\n");
535 }
536
537 /* Create ISO_MAP with projection */
539 if (!isomap) {
540 n_abort("Failed to create ISO_MAP!\n");
541 return -1;
542 }
544 isomap->proj.tile_lift = (float)TILE_LIFT;
545
546 /* Create camera */
547 camera = n_iso_camera_new(0.5f, 6.0f);
548 if (!camera) {
549 n_abort("Failed to create camera!\n");
550 }
551 camera->zoom = 2.0f;
552
553 /* Initialize GUI */
556 // n_gui_set_virtual_size(gui_ctx, (float)SCREEN_W, (float)SCREEN_H);
557
558 /* Modes window */
559 gui_win_modes = n_gui_add_window(gui_ctx, "Modes", 5, 5, 200, 310);
561 10, 10, 180, 24, N_GUI_SHAPE_ROUNDED, 0, on_player_toggle, NULL);
564 10, 40, 180, 24, N_GUI_SHAPE_ROUNDED, 0, on_height_toggle, NULL);
567 10, 70, 180, 24, N_GUI_SHAPE_ROUNDED, 0, on_grid_toggle, NULL);
570 10, 100, 180, 24, N_GUI_SHAPE_ROUNDED, 0, on_smooth_toggle, NULL);
573 10, 130, 180, 24, N_GUI_SHAPE_ROUNDED, 0, on_ghost_toggle, NULL);
576 10, 165, 180, 16, N_GUI_ALIGN_LEFT);
578 10, 185, 150, 20, 0.0, (double)MAX_HEIGHT, 0.0,
579 N_GUI_SLIDER_VALUE, NULL, NULL);
580 gui_btn_reset = n_gui_add_button(gui_ctx, gui_win_modes, "Reset terrain [R]",
581 10, 250, 180, 24, N_GUI_SHAPE_ROUNDED, on_reset_click, NULL);
585
586 /* Tiles window */
587 gui_win_tiles = n_gui_add_window(gui_ctx, "Tiles", 5, 325, 200, 200);
589 N_GUI_SELECT_SINGLE, NULL, NULL);
590 for (int t = 0; t < NUM_TERRAINS; t++)
594 10, 115, 180, 16, N_GUI_ALIGN_LEFT);
596 10, 135, 150, 20, 1.0, (double)MAX_HEIGHT, (double)smooth_slope_max,
597 N_GUI_SLIDER_VALUE, NULL, NULL);
598
599 /* Projection window */
600 gui_win_proj = n_gui_add_window(gui_ctx, "Projection", 5, 535, 200, 160);
602 10, 10, 180, 24, N_GUI_SHAPE_ROUNDED, 1, on_proj_select, NULL);
603 n_gui_button_set_keycode(gui_ctx, gui_btn_proj[0], ALLEGRO_KEY_F1, 0);
605 10, 40, 180, 24, N_GUI_SHAPE_ROUNDED, 0, on_proj_select, NULL);
606 n_gui_button_set_keycode(gui_ctx, gui_btn_proj[1], ALLEGRO_KEY_F2, 0);
608 10, 70, 180, 24, N_GUI_SHAPE_ROUNDED, 0, on_proj_select, NULL);
609 n_gui_button_set_keycode(gui_ctx, gui_btn_proj[2], ALLEGRO_KEY_F3, 0);
611 10, 100, 180, 24, N_GUI_SHAPE_ROUNDED, 0, on_proj_select, NULL);
612 n_gui_button_set_keycode(gui_ctx, gui_btn_proj[3], ALLEGRO_KEY_F4, 0);
613
614 /* Info window */
615 gui_win_info = n_gui_add_window(gui_ctx, "Info", 210, 5, 420, 90);
617 gui_lbl_proj = n_gui_add_label(gui_ctx, gui_win_info, "Projection: Classic 2:1", 10, 22, 400, 16, N_GUI_ALIGN_LEFT);
619
620 /* Ghost dead reckoning window */
621 gui_win_ghost_dr = n_gui_add_window(gui_ctx, "Ghost DR", 1070, 5, 200, 340);
623 10, 5, 180, 16, N_GUI_ALIGN_LEFT);
625 10, 23, 180, 66, NULL, NULL);
630
632 10, 93, 180, 16, N_GUI_ALIGN_LEFT);
634 10, 111, 180, 66, NULL, NULL);
639
641 10, 181, 180, 16, N_GUI_ALIGN_LEFT);
643 10, 199, 180, 20, 0.05, 2.0, ghost_send_interval,
644 N_GUI_SLIDER_VALUE, NULL, NULL);
645
647 10, 225, 180, 16, N_GUI_ALIGN_LEFT);
649 10, 243, 180, 20, 0.01, 2.0, 0.2,
650 N_GUI_SLIDER_VALUE, NULL, NULL);
651
653 10, 269, 180, 16, N_GUI_ALIGN_LEFT);
655 10, 287, 180, 20, 0.01, 5.0, 0.5,
656 N_GUI_SLIDER_VALUE, NULL, NULL);
657
658 /* Start hidden; shown when ghost is enabled */
660
661 /* Load tile images */
662 int saved_bmp_flags = al_get_new_bitmap_flags();
663 al_set_new_bitmap_flags(saved_bmp_flags & ~(ALLEGRO_MIN_LINEAR | ALLEGRO_MAG_LINEAR));
664 for (int i = 0; i < NUM_TERRAINS; i++) {
665 tile_bitmaps[i] = al_load_bitmap(terrain_files[i]);
666 if (!tile_bitmaps[i]) {
667 n_log(LOG_ERR, "Failed to load tile: %s", terrain_files[i]);
668 return EXIT_FAILURE;
669 }
671 }
672
673 /* Generate transition data (nilorea-library) */
675 {
676 ALLEGRO_BITMAP** trans_ptrs[NUM_TERRAINS];
677 for (int t = 0; t < NUM_TERRAINS; t++)
678 trans_ptrs[t] = transition_tiles[t];
681 }
682 al_set_new_bitmap_flags(saved_bmp_flags);
683
684 /* Initialize map: try to load from file, else randomize */
685 {
686 ISO_MAP* loaded = iso_map_load(MAP_FILE);
687 if (loaded) {
688 for (int y = 0; y < MAP_H && y < loaded->height; y++)
689 for (int x = 0; x < MAP_W && x < loaded->width; x++) {
690 iso_map_set_terrain(isomap, x, y, iso_map_get_terrain(loaded, x, y));
691 iso_map_set_height(isomap, x, y, iso_map_get_height(loaded, x, y));
692 }
693 iso_map_free(&loaded);
694 } else {
697 }
698 }
699
700 /* Center camera */
701 {
702 float sx_center, sy_center;
703 iso_map_to_screen(isomap, MAP_W / 2, MAP_H / 2, 0, &sx_center, &sy_center);
704 n_iso_camera_center_on(camera, sx_center, sy_center, screen_w, screen_h);
705 }
706
707 /* Initialize dead reckoning ghost entity */
709 if (ghost_dr) {
711 dr_entity_set_position(ghost_dr, &init_pos, NULL, NULL, 0.0);
712 }
713
714 /* Event queue and timer */
715 ALLEGRO_TIMER* timer = al_create_timer(1.0 / FPS);
716 ALLEGRO_EVENT_QUEUE* queue = al_create_event_queue();
717 al_register_event_source(queue, al_get_display_event_source(display));
718 al_register_event_source(queue, al_get_timer_event_source(timer));
719 al_register_event_source(queue, al_get_keyboard_event_source());
720 al_register_event_source(queue, al_get_mouse_event_source());
721
722 al_start_timer(timer);
723
724 bool key_up = false, key_down = false, key_left = false, key_right = false;
725 bool key_space = false;
726 float scroll_speed = 4.0f;
727 float dt = 1.0f / (float)FPS;
728
729 /* Initialize player position */
730 {
732 player_z = interp_h;
734 }
735
736 /* Main loop */
737 while (running) {
738 ALLEGRO_EVENT ev;
739 al_wait_for_event(queue, &ev);
740
741 /* pass events to the GUI.
742 * n_gui_process_event returns 1 when the event was consumed by GUI.
743 * n_gui_wants_mouse() is also checked below before game mouse actions. */
744 int gui_consumed = n_gui_process_event(gui_ctx, ev);
745 (void)gui_consumed;
746
747 switch (ev.type) {
748 case ALLEGRO_EVENT_TIMER: {
749 /* Smooth projection transition */
751
752 /* A* path following */
754 /* For the last node, target the exact click position;
755 * for intermediate nodes, target tile centers. */
756 float target_fx, target_fy;
757 int is_last_node = (player_path_idx == player_path->length - 1);
758 if (is_last_node) {
759 target_fx = player_click_fx;
760 target_fy = player_click_fy;
761 } else {
762 target_fx = (float)player_path->nodes[player_path_idx].x + 0.5f;
763 target_fy = (float)player_path->nodes[player_path_idx].y + 0.5f;
764 }
765 float path_dx = target_fx - player_fx;
766 float path_dy = target_fy - player_fy;
767 float path_dist = sqrtf(path_dx * path_dx + path_dy * path_dy);
768 float step = PLAYER_MOVE_SPEED * dt;
769 if (path_dist <= step) {
770 player_fx = target_fx;
771 player_fy = target_fy;
775 if (player_path_idx >= player_path->length) {
777 player_path = NULL;
778 }
779 } else {
780 player_fx += (path_dx / path_dist) * step;
781 player_fy += (path_dy / path_dist) * step;
782 player_mx = (int)floorf(player_fx);
783 player_my = (int)floorf(player_fy);
784 }
785 }
786
787 /* Player free movement update */
788 if (player_mode) {
789 float move_dx = 0.0f, move_dy = 0.0f;
790 if (key_up) move_dy -= 1.0f;
791 if (key_down) move_dy += 1.0f;
792 if (key_left) move_dx -= 1.0f;
793 if (key_right) move_dx += 1.0f;
794
795 if (move_dx != 0.0f && move_dy != 0.0f) {
796 move_dx *= 0.70710678f;
797 move_dy *= 0.70710678f;
798 }
799
800 /* Jump */
801 if (key_space && player_on_ground) {
803 player_on_ground = false;
804 }
805
807
808 /* Vertical physics */
809 if (!player_on_ground) {
810 player_vz -= JUMP_GRAVITY * dt;
811 player_z += player_vz * dt;
812 if (player_z <= ground_h) {
813 player_z = ground_h;
814 player_vz = 0.0f;
815 player_on_ground = true;
816 }
817 } else {
818 if (player_z - ground_h > 1.0f) {
819 player_on_ground = false;
820 player_vz = 0.0f;
821 } else {
822 player_z = ground_h;
823 }
824 }
825
826 /* Horizontal movement with collision */
827 if (move_dx != 0.0f || move_dy != 0.0f) {
828 float speed = PLAYER_MOVE_SPEED * dt;
829 float new_fx = player_fx + move_dx * speed;
830 float new_fy = player_fy + move_dy * speed;
831
832 if (new_fx < 0.1f) new_fx = 0.1f;
833 if (new_fx > (float)MAP_W - 0.1f) new_fx = (float)MAP_W - 0.1f;
834 if (new_fy < 0.1f) new_fy = 0.1f;
835 if (new_fy > (float)MAP_H - 0.1f) new_fy = (float)MAP_H - 0.1f;
836
837 int new_tile_x = (int)floorf(new_fx);
838 int new_tile_y = (int)floorf(new_fy);
839
840 bool can_move = true;
841 if (new_tile_x != player_mx || new_tile_y != player_my) {
842 if (new_tile_x >= 0 && new_tile_x < MAP_W &&
843 new_tile_y >= 0 && new_tile_y < MAP_H) {
844 int target_h = iso_map_get_height(isomap, new_tile_x, new_tile_y);
845 int current_h = iso_map_get_height(isomap, player_mx, player_my);
846 if (player_on_ground) {
847 if (target_h - current_h > PLAYER_MAX_HEIGH_DIFF)
848 can_move = false;
849 } else {
850 if ((float)target_h > player_z + 0.5f)
851 can_move = false;
852 }
853 } else {
854 can_move = false;
855 }
856 }
857
858 if (can_move) {
859 player_fx = new_fx;
860 player_fy = new_fy;
861 player_mx = new_tile_x;
862 player_my = new_tile_y;
863 } else {
864 /* Wall sliding: try each axis independently */
865 float slide_fx = player_fx + move_dx * speed;
866 if (slide_fx < 0.1f) slide_fx = 0.1f;
867 if (slide_fx > (float)MAP_W - 0.1f) slide_fx = (float)MAP_W - 0.1f;
868 int slide_tx = (int)floorf(slide_fx);
869 bool can_x = true;
870 if (slide_tx != player_mx) {
871 if (slide_tx >= 0 && slide_tx < MAP_W) {
872 int th = iso_map_get_height(isomap, slide_tx, player_my);
874 if (player_on_ground) {
875 if (th - ch > PLAYER_MAX_HEIGH_DIFF) can_x = false;
876 } else {
877 if ((float)th > player_z + 0.5f) can_x = false;
878 }
879 } else {
880 can_x = false;
881 }
882 }
883 if (can_x) {
884 player_fx = slide_fx;
885 player_mx = slide_tx;
886 }
887
888 float slide_fy = player_fy + move_dy * speed;
889 if (slide_fy < 0.1f) slide_fy = 0.1f;
890 if (slide_fy > (float)MAP_H - 0.1f) slide_fy = (float)MAP_H - 0.1f;
891 int slide_ty = (int)floorf(slide_fy);
892 bool can_y = true;
893 if (slide_ty != player_my) {
894 if (slide_ty >= 0 && slide_ty < MAP_H) {
895 int th = iso_map_get_height(isomap, player_mx, slide_ty);
897 if (player_on_ground) {
898 if (th - ch > PLAYER_MAX_HEIGH_DIFF) can_y = false;
899 } else {
900 if ((float)th > player_z + 0.5f) can_y = false;
901 }
902 } else {
903 can_y = false;
904 }
905 }
906 if (can_y) {
907 player_fy = slide_fy;
908 player_my = slide_ty;
909 }
910 }
911 }
912
913 /* Recompute ground height after movement */
915 if (player_on_ground) {
916 if (player_z - ground_h > 1.0f) {
917 player_on_ground = false;
918 player_vz = 0.0f;
919 } else {
920 player_z = ground_h;
921 }
922 } else if (player_z <= ground_h) {
923 player_z = ground_h;
924 player_vz = 0.0f;
925 player_on_ground = true;
926 }
927
928 /* Update screen position */
930 }
931
932 /* Camera: smoothly follow player in player mode */
933 if (player_mode) {
936 }
937
938 /* Editor mode: scroll camera */
939 if (!player_mode) {
940 float sdx = 0.0f, sdy = 0.0f;
941 if (key_left) sdx += scroll_speed / camera->zoom;
942 if (key_right) sdx -= scroll_speed / camera->zoom;
943 if (key_up) sdy += scroll_speed / camera->zoom;
944 if (key_down) sdy -= scroll_speed / camera->zoom;
945 if (sdx != 0.0f || sdy != 0.0f)
946 n_iso_camera_scroll(camera, sdx, sdy);
947 }
948
949 /* Dead reckoning ghost update */
950 if (ghost_enabled && ghost_dr) {
951 double now = al_get_time();
952
954 if (ghost_path_angle > 2.0f * (float)M_PI)
955 ghost_path_angle -= 2.0f * (float)M_PI;
956
957 float true_fx = ghost_path_cx + ghost_path_radius * cosf(ghost_path_angle);
958 float true_fy = ghost_path_cy + ghost_path_radius * sinf(ghost_path_angle);
959
960 float true_vx = -ghost_path_radius * ghost_path_speed * sinf(ghost_path_angle);
961 float true_vy = ghost_path_radius * ghost_path_speed * cosf(ghost_path_angle);
962
964 float true_ax = -w2r * cosf(ghost_path_angle);
965 float true_ay = -w2r * sinf(ghost_path_angle);
966
967 {
968 DR_VEC3 pos = dr_vec3(true_fx, true_fy, 0.0);
969 DR_VEC3 vel = dr_vec3(true_vx, true_vy, 0.0);
970 DR_VEC3 acc = dr_vec3(true_ax, true_ay, 0.0);
972 dr_entity_check_threshold(ghost_dr, &pos, &vel, &acc, now)) {
973 dr_entity_receive_state(ghost_dr, &pos, &vel, &acc, now);
974 ghost_last_send = now;
975 }
976 }
977
978 DR_VEC3 dr_pos;
979 dr_entity_compute(ghost_dr, now, &dr_pos);
980
981 float dr_h = interpolate_height_at((float)dr_pos.x, (float)dr_pos.y);
982 iso_map_to_screen_f(isomap, (float)dr_pos.x, (float)dr_pos.y, dr_h,
984
985 float true_h = interpolate_height_at(true_fx, true_fy);
986 iso_map_to_screen_f(isomap, true_fx, true_fy, true_h,
988 }
989
990 /* Update hover tile (skip when mouse is over a GUI window) */
992 float wx, wy;
994 (float)last_mouse_y, &wx, &wy);
996 &hover_mx, &hover_my);
997 } else {
998 hover_mx = -1;
999 hover_my = -1;
1000 }
1001
1002 /* GUI <-> variable sync */
1003 {
1004 bool gui_player = n_gui_button_is_toggled(gui_ctx, gui_btn_player) != 0;
1005 bool gui_height = n_gui_button_is_toggled(gui_ctx, gui_btn_height) != 0;
1006 bool gui_grid = n_gui_button_is_toggled(gui_ctx, gui_btn_grid) != 0;
1007 bool gui_smooth = n_gui_button_is_toggled(gui_ctx, gui_btn_smooth) != 0;
1008 bool gui_ghost = n_gui_button_is_toggled(gui_ctx, gui_btn_ghost) != 0;
1009
1010 if (gui_player != player_mode) {
1011 player_mode = gui_player;
1012 if (player_mode) {
1013 int drop_max_h = 0;
1014 for (int y = 0; y < MAP_H; y++)
1015 for (int x = 0; x < MAP_W; x++) {
1016 int h = iso_map_get_height(isomap, x, y);
1017 if (h > drop_max_h) drop_max_h = h;
1018 }
1019 player_z = (float)(drop_max_h + 1);
1020 player_vz = 0.0f;
1021 player_on_ground = false;
1026 }
1027 }
1028 if (gui_height != height_mode) {
1029 height_mode = gui_height;
1030 if (height_mode && player_mode)
1031 player_mode = false;
1032 }
1033 if (gui_grid != show_grid) show_grid = gui_grid;
1034 if (gui_smooth != smooth_height) smooth_height = gui_smooth;
1035 if (gui_ghost != ghost_enabled) {
1036 ghost_enabled = gui_ghost;
1037 if (ghost_enabled) {
1039 if (ghost_dr) {
1040 ghost_last_send = al_get_time();
1041 ghost_path_angle = 0.0f;
1042 int ghost_max_h = 0;
1043 for (int y = 0; y < MAP_H; y++)
1044 for (int x = 0; x < MAP_W; x++) {
1045 int h = iso_map_get_height(isomap, x, y);
1046 if (h > ghost_max_h) ghost_max_h = h;
1047 }
1049 ghost_path_cy, (double)(ghost_max_h + 1));
1050 DR_VEC3 init_vel = dr_vec3(0.0, ghost_path_radius * ghost_path_speed, 0.0);
1051 dr_entity_set_position(ghost_dr, &init_pos, &init_vel, NULL,
1052 al_get_time());
1053 }
1054 } else {
1056 }
1057 }
1058
1059 /* Ghost DR window sync */
1060 if (ghost_enabled && ghost_dr) {
1061 /* Algorithm radiolist */
1063 if (gui_algo >= 0 && gui_algo != (int)ghost_dr->algo)
1066
1067 /* Blend radiolist */
1069 if (gui_blend >= 0 && gui_blend != (int)ghost_dr->blend_mode)
1072
1073 /* Send interval slider */
1074 {
1075 double gui_interval = n_gui_slider_get_value(gui_ctx, gui_sld_dr_interval);
1076 if (fabs(gui_interval - ghost_send_interval) > 0.001)
1077 ghost_send_interval = gui_interval;
1079 char ibuf[48];
1080 snprintf(ibuf, sizeof(ibuf), "Interval [,/.]: %.2fs", ghost_send_interval);
1082 }
1083
1084 /* Blend time slider */
1085 {
1087 if (fabs(gui_bt - ghost_dr->blend_time) > 0.001)
1090 char btbuf[48];
1091 snprintf(btbuf, sizeof(btbuf), "Blend time: %.2fs", ghost_dr->blend_time);
1093 }
1094
1095 /* Threshold slider */
1096 {
1098 if (fabs(gui_th - ghost_dr->pos_threshold) > 0.001)
1101 char thbuf[48];
1102 snprintf(thbuf, sizeof(thbuf), "Threshold: %.2f", ghost_dr->pos_threshold);
1104 }
1105 }
1106
1107 /* Height slider */
1108 {
1109 int gui_h = (int)(n_gui_slider_get_value(gui_ctx, gui_sld_height) + 0.5);
1110 if (gui_h != paint_height) paint_height = gui_h;
1112 char hbuf[32];
1113 snprintf(hbuf, sizeof(hbuf), "Height: %d", paint_height);
1117 }
1118
1119 /* Tile listbox */
1120 {
1122 if (gui_sel >= 0 && gui_sel < NUM_TERRAINS && gui_sel != paint_terrain)
1123 paint_terrain = gui_sel;
1125 }
1126
1127 /* Slope slider */
1128 {
1129 int gui_slope = (int)(n_gui_slider_get_value(gui_ctx, gui_sld_slope) + 0.5);
1130 if (gui_slope != smooth_slope_max) smooth_slope_max = gui_slope;
1132 char sbuf2[32];
1133 snprintf(sbuf2, sizeof(sbuf2), "Slope max: %d", smooth_slope_max);
1135 }
1136
1137 /* Sync variables -> GUI */
1143
1144 /* Projection radio group */
1145 for (int p = 0; p < NUM_PROJECTIONS; p++) {
1147 current_proj = p;
1149 break;
1150 }
1151 }
1152 for (int p = 0; p < NUM_PROJECTIONS; p++)
1154 }
1155
1156 redraw_needed = true;
1157 break;
1158 }
1159
1160 case ALLEGRO_EVENT_KEY_DOWN:
1161 switch (ev.keyboard.keycode) {
1162 /* Movement keys (not GUI buttons) */
1163 case ALLEGRO_KEY_LEFT:
1164 case ALLEGRO_KEY_A:
1165 key_left = true;
1166 break;
1167 case ALLEGRO_KEY_RIGHT:
1168 case ALLEGRO_KEY_D:
1169 key_right = true;
1170 break;
1171 case ALLEGRO_KEY_UP:
1172 case ALLEGRO_KEY_W:
1173 key_up = true;
1174 break;
1175 case ALLEGRO_KEY_DOWN:
1176 case ALLEGRO_KEY_S:
1177 key_down = true;
1178 break;
1179 case ALLEGRO_KEY_SPACE:
1180 key_space = true;
1181 break;
1182 case ALLEGRO_KEY_ESCAPE:
1183 running = false;
1184 break;
1185 case ALLEGRO_KEY_PGDN:
1188 break;
1189 case ALLEGRO_KEY_PGUP:
1192 break;
1193 case ALLEGRO_KEY_EQUALS:
1194 case ALLEGRO_KEY_PAD_PLUS:
1196 break;
1197 case ALLEGRO_KEY_MINUS:
1198 case ALLEGRO_KEY_PAD_MINUS:
1199 if (paint_height > 0) paint_height--;
1200 break;
1201 case ALLEGRO_KEY_M:
1202 if (ghost_dr) {
1203 int a = (int)((ghost_dr->algo + 1) % 3);
1206 }
1207 break;
1208 case ALLEGRO_KEY_B:
1209 if (ghost_dr) {
1210 int b = (int)((ghost_dr->blend_mode + 1) % 3);
1213 }
1214 break;
1215 case ALLEGRO_KEY_COMMA:
1216 ghost_send_interval += 0.05;
1219 break;
1220 case ALLEGRO_KEY_FULLSTOP:
1221 ghost_send_interval -= 0.05;
1222 if (ghost_send_interval < 0.05) ghost_send_interval = 0.05;
1224 break;
1225 /* Keys P, H, G, V, N, F1-F4 are handled via
1226 n_gui_button_set_keycode bindings */
1227 }
1228 break;
1229
1230 case ALLEGRO_EVENT_KEY_UP:
1231 switch (ev.keyboard.keycode) {
1232 case ALLEGRO_KEY_LEFT:
1233 case ALLEGRO_KEY_A:
1234 key_left = false;
1235 break;
1236 case ALLEGRO_KEY_RIGHT:
1237 case ALLEGRO_KEY_D:
1238 key_right = false;
1239 break;
1240 case ALLEGRO_KEY_UP:
1241 case ALLEGRO_KEY_W:
1242 key_up = false;
1243 break;
1244 case ALLEGRO_KEY_DOWN:
1245 case ALLEGRO_KEY_S:
1246 key_down = false;
1247 break;
1248 case ALLEGRO_KEY_SPACE:
1249 key_space = false;
1250 break;
1251 }
1252 break;
1253
1254 case ALLEGRO_EVENT_MOUSE_AXES:
1255 last_mouse_x = ev.mouse.x;
1256 last_mouse_y = ev.mouse.y;
1257 if (ev.mouse.dz != 0 && !n_gui_wants_mouse(gui_ctx)) {
1258 n_iso_camera_zoom(camera, (float)ev.mouse.dz * 0.25f,
1259 (float)ev.mouse.x, (float)ev.mouse.y);
1260 }
1261 break;
1262
1263 case ALLEGRO_EVENT_MOUSE_BUTTON_DOWN:
1264 if (ev.mouse.button == 1 && !n_gui_wants_mouse(gui_ctx)) {
1265 float wx, wy;
1266 n_iso_camera_screen_to_world(camera, (float)ev.mouse.x,
1267 (float)ev.mouse.y, &wx, &wy);
1268 int map_x, map_y;
1269 float click_fx = 0.0f, click_fy = 0.0f;
1271 &map_x, &map_y, &click_fx, &click_fy);
1272 if (map_x >= 0 && map_x < MAP_W && map_y >= 0 && map_y < MAP_H) {
1273 if (player_mode) {
1274 /* Click-to-move via A* pathfinding */
1275 if (player_path) {
1277 player_path = NULL;
1278 }
1279 ASTAR_GRID* grid = iso_map_to_astar_grid(isomap, PLAYER_MAX_HEIGH_DIFF, player_mx, player_my);
1280 if (grid) {
1281 int dest_x = map_x, dest_y = map_y;
1282 /* If destination is blocked, find nearest walkable tile */
1283 if (!n_astar_grid_get_walkable(grid, dest_x, dest_y, 0)) {
1284 int found = 0;
1285 for (int ring = 1; ring <= MAX(MAP_W, MAP_H) && !found; ring++) {
1286 for (int ry = -ring; ry <= ring && !found; ry++)
1287 for (int rx = -ring; rx <= ring && !found; rx++) {
1288 if (abs(rx) != ring && abs(ry) != ring) continue;
1289 int nx = map_x + rx, ny = map_y + ry;
1290 if (nx >= 0 && nx < MAP_W && ny >= 0 && ny < MAP_H &&
1291 n_astar_grid_get_walkable(grid, nx, ny, 0)) {
1292 dest_x = nx;
1293 dest_y = ny;
1294 found = 1;
1295 }
1296 }
1297 }
1298 }
1300 player_mx, player_my, 0,
1301 dest_x, dest_y, 0,
1303 n_astar_grid_free(grid);
1304 if (player_path && player_path->length > 1) {
1305 player_path_idx = 1;
1306 player_path_progress = 0.0f;
1307 /* If dest tile matches the clicked tile, use exact
1308 * click position; otherwise center of dest tile. */
1309 int final_tx = player_path->nodes[player_path->length - 1].x;
1310 int final_ty = player_path->nodes[player_path->length - 1].y;
1311 if (final_tx == map_x && final_ty == map_y) {
1312 player_click_fx = click_fx;
1313 player_click_fy = click_fy;
1314 } else {
1315 player_click_fx = (float)final_tx + 0.5f;
1316 player_click_fy = (float)final_ty + 0.5f;
1317 }
1318 /* Clamp to valid map range for border tiles.
1319 * Allow the full extent of the last tile so the
1320 * player can walk anywhere within border tiles. */
1321 if (player_click_fx < 0.0f) player_click_fx = 0.0f;
1322 if (player_click_fy < 0.0f) player_click_fy = 0.0f;
1323 if (player_click_fx > (float)MAP_W - 1e-4f) player_click_fx = (float)MAP_W - 1e-4f;
1324 if (player_click_fy > (float)MAP_H - 1e-4f) player_click_fy = (float)MAP_H - 1e-4f;
1325 } else {
1326 if (player_path) {
1328 player_path = NULL;
1329 }
1330 }
1331 }
1332 } else if (height_mode) {
1333 iso_map_set_height(isomap, map_x, map_y, paint_height);
1335 } else {
1338 }
1339 }
1340 }
1341 break;
1342
1343 case ALLEGRO_EVENT_DISPLAY_CLOSE:
1344 running = false;
1345 break;
1346
1347 case ALLEGRO_EVENT_DISPLAY_RESIZE:
1348 al_acknowledge_resize(display);
1349 screen_w = al_get_display_width(display);
1350 screen_h = al_get_display_height(display);
1352 break;
1353 }
1354
1355 /* Render */
1356 if (redraw_needed && al_is_event_queue_empty(queue)) {
1357 redraw_needed = false;
1358 al_set_target_backbuffer(display);
1359 al_clear_to_color(al_map_rgb(20, 25, 30));
1360
1361 /* Sync ISO_MAP rendering flags */
1362 isomap->smooth_height = smooth_height ? 1 : 0;
1363 isomap->smooth_slope_max = smooth_slope_max;
1364 isomap->show_grid = show_grid ? 1 : 0;
1365 isomap->hover_mx = hover_mx;
1366 isomap->hover_my = hover_my;
1367
1368 /* Build object list for depth-sorted rendering */
1369 N_ISO_OBJECT iso_objects[2];
1370 int iso_num_objects = 0;
1371
1372 if (player_mode) {
1373 iso_objects[iso_num_objects].fx = player_fx;
1374 iso_objects[iso_num_objects].fy = player_fy;
1375 iso_objects[iso_num_objects].fz = player_z;
1376 iso_objects[iso_num_objects].occluded_alpha = 0.35f;
1377 iso_objects[iso_num_objects].is_occluded = 0;
1378 iso_objects[iso_num_objects].draw = draw_player_object;
1379 iso_objects[iso_num_objects].user_data = NULL;
1380 iso_num_objects++;
1381 }
1382
1383 if (ghost_enabled && ghost_dr) {
1384 /* ghost uses the DR-computed screen pos; derive map pos */
1385 DR_VEC3 dr_pos;
1386 dr_entity_compute(ghost_dr, al_get_time(), &dr_pos);
1387 float dr_h = interpolate_height_at((float)dr_pos.x, (float)dr_pos.y);
1388 iso_objects[iso_num_objects].fx = (float)dr_pos.x;
1389 iso_objects[iso_num_objects].fy = (float)dr_pos.y;
1390 iso_objects[iso_num_objects].fz = dr_h;
1391 iso_objects[iso_num_objects].occluded_alpha = 0.35f;
1392 iso_objects[iso_num_objects].is_occluded = 0;
1393 iso_objects[iso_num_objects].draw = draw_ghost_object;
1394 iso_objects[iso_num_objects].user_data = NULL;
1395 iso_num_objects++;
1396 }
1397
1398 /* Draw map with depth-sorted objects */
1399 {
1400 ALLEGRO_BITMAP** trans_ptrs[NUM_TERRAINS];
1401 for (int t = 0; t < NUM_TERRAINS; t++)
1402 trans_ptrs[t] = transition_tiles[t];
1404 NULL, 0,
1405 floorf(camera->x * camera->zoom),
1406 floorf(camera->y * camera->zoom),
1407 camera->zoom,
1408 screen_w, screen_h, player_mode ? 1 : 0,
1409 iso_num_objects > 0 ? iso_objects : NULL,
1410 iso_num_objects);
1411 }
1412
1413 /* Update GUI info labels */
1414 {
1415 char sbuf[256];
1416 if (player_mode) {
1418 snprintf(sbuf, sizeof(sbuf), "Pos:(%.1f,%.1f) Z:%.1f GndH:%.1f %s | Zoom:%.1fx",
1420 player_on_ground ? "GND" : "AIR", camera->zoom);
1421 } else if (height_mode) {
1422 snprintf(sbuf, sizeof(sbuf), "Paint height:%d | Zoom:%.1fx | Slope max:%d",
1424 } else {
1425 snprintf(sbuf, sizeof(sbuf), "Brush:[%s] | Zoom:%.1fx | Slope max:%d",
1427 }
1429
1430 snprintf(sbuf, sizeof(sbuf), "Proj: %s (%.1f deg)",
1431 iso_projection_name(current_proj), isomap->proj.angle_deg);
1433
1435 snprintf(sbuf, sizeof(sbuf), "Hover: (%d,%d) H:%d T:%s",
1440 }
1441 }
1442
1443 /* Draw GUI overlay */
1445
1446 al_flip_display();
1447 }
1448 }
1449
1450 /* Cleanup */
1452 if (player_path) {
1454 player_path = NULL;
1455 }
1458 if (isomap) iso_map_free(&isomap);
1459
1460 for (int t = 0; t < NUM_TERRAINS; t++) {
1461 for (int m = 0; m < NUM_MASKS; m++)
1462 if (transition_tiles[t][m]) al_destroy_bitmap(transition_tiles[t][m]);
1463 if (tile_bitmaps[t]) al_destroy_bitmap(tile_bitmaps[t]);
1464 }
1465 for (int m = 0; m < NUM_MASKS; m++)
1466 if (transition_masks[m]) al_destroy_bitmap(transition_masks[m]);
1467
1468 al_destroy_font(font);
1469 al_destroy_timer(timer);
1470 al_destroy_event_queue(queue);
1471 al_destroy_display(display);
1472
1473 return EXIT_SUCCESS;
1474}
int main(void)
Definition ex_atlas.c:117
ALLEGRO_DISPLAY * display
Definition ex_fluid.c:54
void on_grid_toggle(int widget_id, void *user_data)
Definition ex_gui.c:211
void on_player_toggle(int widget_id, void *user_data)
Definition ex_gui.c:199
static int smooth_height
Definition ex_gui.c:65
void on_smooth_toggle(int widget_id, void *user_data)
Definition ex_gui.c:217
static int show_grid
Definition ex_gui.c:64
void on_ghost_toggle(int widget_id, void *user_data)
Definition ex_gui.c:223
void on_proj_select(int widget_id, void *user_data)
Definition ex_gui.c:230
static int player_mode
Definition ex_gui.c:62
void on_height_toggle(int widget_id, void *user_data)
Definition ex_gui.c:205
static int ghost_enabled
Definition ex_gui.c:66
static int height_mode
Definition ex_gui.c:63
static int current_proj
Definition ex_gui.c:67
#define MAP_H
static int gui_btn_ghost
static float interpolate_height_at_clamped(float fx, float fy, int max_climb)
static int paint_terrain
#define TILE_LIFT
static int last_mouse_y
static int gui_btn_smooth
static void draw_ghost_object(float sx, float sy, float zoom, float alpha, void *user_data)
static float ghost_path_angle
#define TILE_W
static int player_path_idx
#define TERRAIN_LAVE
static ASTAR_PATH * player_path
static void on_reset_click(int id, void *data)
static int gui_sld_dr_blend_time
#define MAP_FILE
static int gui_sld_dr_threshold
static int hover_mx
static double ghost_last_send
static int gui_sld_height
static int gui_win_proj
static int gui_win_modes
static int gui_sld_slope
static float ghost_path_radius
static bool running
static void draw_player_object(float sx, float sy, float zoom, float alpha, void *user_data)
static N_ISO_CAMERA * camera
static void randomize_map(void)
#define MAP_W
static int gui_sld_dr_interval
static float player_path_progress
static int gui_btn_proj[4]
static int screen_w
static int gui_win_ghost_dr
static float player_click_fy
static ALLEGRO_BITMAP * transition_tiles[8][(16+16)]
static ISO_MAP * isomap
static int smooth_slope_max
#define JUMP_GRAVITY
static int player_mx
static int gui_lbl_dr_threshold
static float player_screen_y
static int gui_lbl_slope
static float ghost_path_speed
static float player_screen_x
static float ghost_path_cy
static int gui_btn_player
static ALLEGRO_BITMAP * tile_bitmaps[8]
static int gui_win_info
static int player_my
static bool redraw_needed
static const char * terrain_files[8]
static float ghost_screen_x
static int gui_btn_reset
static int gui_lbl_hover
static int gui_lbl_dr_algo
#define NUM_PROJECTIONS
static int gui_lst_tiles
static float ghost_true_y
static int gui_rad_dr_blend
#define MAX_HEIGHT
#define JUMP_VELOCITY
static int gui_btn_height
static float ghost_path_cx
static float player_click_fx
static int gui_lbl_proj
#define PLAYER_MOVE_SPEED
#define PLAYER_MAX_HEIGH_DIFF
static int gui_lbl_dr_interval
static int hover_my
static DR_ENTITY * ghost_dr
#define TERRAIN_TEMPLE3
#define TILE_H
static float player_fy
static float ghost_screen_y
static float player_z
static int last_mouse_x
static int paint_height
static double ghost_send_interval
#define FPS
static float player_vz
static float ghost_true_x
#define CAM_SMOOTHING
static const char * terrain_names[8]
static float player_fx
static int screen_h
static int gui_lbl_dr_blend_time
static int gui_lbl_dr_blend
#define NUM_TERRAINS
static ALLEGRO_BITMAP * transition_masks[(16+16)]
static int gui_win_tiles
static int gui_lbl_height
static int gui_rad_dr_algo
static int gui_btn_grid
static float interpolate_height_at(float fx, float fy)
#define NUM_MASKS
static int gui_lbl_status
static N_GUI_CTX * gui_ctx
static bool player_on_ground
#define M_PI
#define SCREEN_H
screen height
FONT * font
the default 8x8 font, valid after allegro_init()
#define SCREEN_W
screen width
ASTAR_PATH * n_astar_find_path(const ASTAR_GRID *grid, int sx, int sy, int sz, int gx, int gy, int gz, int diagonal, ASTAR_HEURISTIC heuristic)
Find a path using A* search.
Definition n_astar.c:485
uint8_t n_astar_grid_get_walkable(const ASTAR_GRID *grid, int x, int y, int z)
Get a cell's walkability.
Definition n_astar.c:299
#define ASTAR_ALLOW_DIAGONAL
Movement mode: 8-dir (2D) or 26-dir (3D).
Definition n_astar.h:76
void n_astar_grid_free(ASTAR_GRID *grid)
Free a grid and all its internal data.
Definition n_astar.c:271
void n_astar_path_free(ASTAR_PATH *path)
Free a path returned by n_astar_find_path.
Definition n_astar.c:759
@ ASTAR_HEURISTIC_CHEBYSHEV
max of axis deltas (optimal for 8-dir)
Definition n_astar.h:109
Grid structure holding walkability, costs, and dimensions.
Definition n_astar.h:153
The computed path result.
Definition n_astar.h:122
void n_abort(char const *format,...)
abort program with a text
Definition n_common.c:57
double x
X component.
double y
Y component.
void dr_entity_destroy(DR_ENTITY **entity_ptr)
Destroy a dead reckoning entity and set the pointer to NULL.
static DR_VEC3 dr_vec3(double x, double y, double z)
Create a DR_VEC3 from components.
void dr_entity_set_threshold(DR_ENTITY *entity, double threshold)
Set the position error threshold for triggering network updates.
void dr_entity_compute(DR_ENTITY *entity, double time, DR_VEC3 *out_pos)
Compute the dead reckoned display position at a given time.
DR_ENTITY * dr_entity_create(DR_ALGO algo, DR_BLEND blend_mode, double pos_threshold, double blend_time)
Create a new dead reckoning entity.
void dr_entity_set_blend_time(DR_ENTITY *entity, double blend_time)
Set the convergence blend duration.
DR_BLEND
Dead reckoning convergence/blending mode.
void dr_entity_set_blend_mode(DR_ENTITY *entity, DR_BLEND blend_mode)
Set the convergence blending mode.
bool dr_entity_check_threshold(const DR_ENTITY *entity, const DR_VEC3 *true_pos, const DR_VEC3 *true_vel, const DR_VEC3 *true_acc, double time)
Check whether the owner's true state has diverged from the dead reckoned prediction beyond the config...
DR_ALGO
Dead reckoning extrapolation algorithm.
void dr_entity_set_algo(DR_ENTITY *entity, DR_ALGO algo)
Set the extrapolation algorithm.
void dr_entity_receive_state(DR_ENTITY *entity, const DR_VEC3 *pos, const DR_VEC3 *vel, const DR_VEC3 *acc, double time)
Receive a new authoritative state update from the network.
void dr_entity_set_position(DR_ENTITY *entity, const DR_VEC3 *pos, const DR_VEC3 *vel, const DR_VEC3 *acc, double time)
Force-set entity position without triggering convergence blending.
@ DR_BLEND_PVB
Projective Velocity Blending (recommended).
@ DR_ALGO_VEL_ACC
Velocity + acceleration: P(t) = P0 + V0*t + 0.5*A0*t^2.
Dead reckoned entity with extrapolation and convergence state.
3D vector used for position, velocity, and acceleration
void n_gui_set_display_size(N_GUI_CTX *ctx, float w, float h)
set the display (viewport) size so global scrollbars can be computed.
Definition n_gui.c:12348
int n_gui_button_is_toggled(N_GUI_CTX *ctx, int widget_id)
Check if a toggle button is currently in the "on" state.
Definition n_gui.c:2806
#define N_GUI_ALIGN_LEFT
left aligned text
Definition n_gui.h:212
int n_gui_add_slider(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, double min_val, double max_val, double initial, int mode, void(*on_change)(int, double, void *), void *user_data)
Add a slider widget.
Definition n_gui.c:3106
int n_gui_wants_mouse(N_GUI_CTX *ctx)
Check if the mouse is currently over any open GUI window.
Definition n_gui.c:16558
int n_gui_radiolist_add_item(N_GUI_CTX *ctx, int widget_id, const char *text)
add an item to a radiolist widget
Definition n_gui.c:5930
void n_gui_label_set_text(N_GUI_CTX *ctx, int widget_id, const char *text)
set label text content
Definition n_gui.c:6184
int n_gui_add_listbox(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, int selection_mode, void(*on_select)(int, int, int, void *), void *user_data)
Add a listbox widget.
Definition n_gui.c:3285
#define N_GUI_SLIDER_VALUE
slider uses raw start/end values
Definition n_gui.h:178
int n_gui_process_event(N_GUI_CTX *ctx, ALLEGRO_EVENT event)
Process an allegro event through the GUI system.
Definition n_gui.c:13611
int n_gui_add_label(N_GUI_CTX *ctx, int window_id, const char *text, float x, float y, float w, float h, int align)
Add a static text label.
Definition n_gui.c:3639
int n_gui_add_radiolist(N_GUI_CTX *ctx, int window_id, float x, float y, float w, float h, void(*on_select)(int, int, void *), void *user_data)
Add a radio list widget (single selection with radio bullets).
Definition n_gui.c:3512
void n_gui_close_window(N_GUI_CTX *ctx, int window_id)
Close (hide) a window.
Definition n_gui.c:1406
void n_gui_listbox_set_selected(N_GUI_CTX *ctx, int widget_id, int index, int selected)
set the selection state of a listbox item
Definition n_gui.c:4491
double n_gui_slider_get_value(N_GUI_CTX *ctx, int widget_id)
get the current value of a slider widget
Definition n_gui.c:3962
int n_gui_listbox_add_item(N_GUI_CTX *ctx, int widget_id, const char *text)
add an item to a listbox widget
Definition n_gui.c:4359
int n_gui_radiolist_get_selected(N_GUI_CTX *ctx, int widget_id)
get the selected item index in a radiolist widget
Definition n_gui.c:5967
#define N_GUI_SELECT_SINGLE
single item selection
Definition n_gui.h:198
void n_gui_button_set_toggled(N_GUI_CTX *ctx, int widget_id, int toggled)
Set the toggle state of a button.
Definition n_gui.c:3043
int n_gui_add_toggle_button(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, int shape, int initial_state, void(*on_click)(int, void *), void *user_data)
Add a toggle button widget (stays clicked/unclicked on single click).
Definition n_gui.c:2790
int n_gui_listbox_get_selected(N_GUI_CTX *ctx, int widget_id)
get the index of the first selected item in a listbox
Definition n_gui.c:4455
void n_gui_draw(N_GUI_CTX *ctx)
draw all visible GUI windows and widgets
Definition n_gui.c:11735
void n_gui_button_set_keycode(N_GUI_CTX *ctx, int widget_id, int keycode, int modifiers)
Bind a keyboard key with optional modifier requirements to a button.
Definition n_gui.c:3075
int n_gui_add_window(N_GUI_CTX *ctx, const char *title, float x, float y, float w, float h)
Add a new pseudo-window to the context.
Definition n_gui.c:1277
N_GUI_CTX * n_gui_new_ctx(ALLEGRO_FONT *default_font)
Create a new GUI context.
Definition n_gui.c:1103
void n_gui_destroy_ctx(N_GUI_CTX **ctx)
Destroy a GUI context and all its windows/widgets.
Definition n_gui.c:1209
void n_gui_open_window(N_GUI_CTX *ctx, int window_id)
Open (show) a window.
Definition n_gui.c:1436
void n_gui_slider_set_value(N_GUI_CTX *ctx, int widget_id, double value)
set the value of a slider widget
Definition n_gui.c:3976
#define N_GUI_SHAPE_ROUNDED
rounded rectangle shape
Definition n_gui.h:172
void n_gui_radiolist_set_selected(N_GUI_CTX *ctx, int widget_id, int index)
set the selected item in a radiolist widget
Definition n_gui.c:5981
void n_gui_set_widget_visible(N_GUI_CTX *ctx, int widget_id, int visible)
Show or hide a widget.
Definition n_gui.c:3828
int n_gui_add_button(N_GUI_CTX *ctx, int window_id, const char *label, float x, float y, float w, float h, int shape, void(*on_click)(int, void *), void *user_data)
Add a button widget to a window.
Definition n_gui.c:2720
The top-level GUI context that holds all windows.
Definition n_gui.h:1669
float fz
height in map units (elevation)
float fy
fractional map Y position
int height
map height in tiles (Y axis)
float occluded_alpha
overlay alpha when behind tiles [0..1], 0=hidden, 0.35=ghost, 1=always visible
float fx
fractional map X position
void * user_data
user data passed to draw callback
N_ISO_OBJECT_DRAW_FN draw
draw callback
int is_occluded
OUTPUT: set to 1 if behind tiles during last iso_map_draw() call.
int width
map width in tiles (X axis)
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]).
ISO_MAP * iso_map_new(int width, int height, int num_terrains, int max_height)
Create a new height-aware isometric map.
void n_iso_camera_zoom(N_ISO_CAMERA *cam, float dz, float mouse_x, float mouse_y)
Zoom the camera toward a screen-space point.
void n_iso_camera_center_on(N_ISO_CAMERA *cam, float world_x, float world_y, int screen_w, int screen_h)
Center the camera so that a world point is at screen center.
int iso_map_save(const ISO_MAP *map, const char *filename)
Save ISO_MAP to a binary file.
void iso_map_lerp_projection(ISO_MAP *map, float dt)
Smoothly interpolate the projection angle toward the target.
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_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.
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.
int iso_map_get_terrain(const ISO_MAP *map, int mx, int my)
Get the terrain type at a cell.
void iso_map_draw(const ISO_MAP *map, ALLEGRO_BITMAP **tile_bitmaps, ALLEGRO_BITMAP ***transition_tiles, int num_masks, ALLEGRO_BITMAP **overlay_bitmaps, int num_overlay_tiles, float cam_px, float cam_py, float zoom, int screen_w, int screen_h, int player_mode, N_ISO_OBJECT *objects, int num_objects)
Draw the full ISO_MAP with segment-sorted rendering.
void iso_map_to_screen(const ISO_MAP *map, int mx, int my, int h, float *screen_x, float *screen_y)
Convert map tile coordinates to screen pixel coordinates.
void iso_map_set_projection(ISO_MAP *map, int preset, float tile_width)
Set projection parameters from a preset and tile width.
void n_iso_camera_screen_to_world(const N_ISO_CAMERA *cam, float sx, float sy, float *wx, float *wy)
Convert screen pixel coordinates to world coordinates.
void iso_map_to_screen_f(const ISO_MAP *map, float fmx, float fmy, float h, float *screen_x, float *screen_y)
Convert map coordinates to screen coordinates (float version).
void iso_map_free(ISO_MAP **map_ptr)
Free an ISO_MAP and set the pointer to NULL.
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).
void iso_map_set_projection_target(ISO_MAP *map, int preset)
Set the target projection for smooth interpolation.
void iso_map_set_terrain(ISO_MAP *map, int mx, int my, int terrain)
Set the terrain type at a cell.
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).
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 n_iso_camera_scroll(N_ISO_CAMERA *cam, float dx, float dy)
Scroll the camera by (dx, dy) world units.
N_ISO_CAMERA * n_iso_camera_new(float zoom_min, float zoom_max)
Create a new 2D isometric camera.
Height-aware isometric map with terrain and height layers, per-cell height values,...
2D isometric camera for viewport management.
Drawable object for depth-sorted isometric rendering.
#define n_log(__LEVEL__,...)
Logging function wrapper to get line and func.
Definition n_log.h:113
#define LOG_ERR
error conditions
Definition n_log.h:100
void set_log_level(const int log_level)
Set the global log level value ( static int LOG_LEVEL ).
Definition n_log.c:123
#define LOG_INFO
informational
Definition n_log.h:106
A* Pathfinding API for 2D and 3D grids.
Common headers and low-level functions & define.
Dead Reckoning API for latency hiding in networked games.
GUI system: buttons, sliders, text areas, checkboxes, scrollbars, dropdown menus, windows.
Isometric/axonometric tile engine with height maps, terrain transitions, and A* pathfinding integrati...
List structures and definitions.
Generic log system.
N_STR and string function declaration.