diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index aabc1e3..d3045cb 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -85,6 +85,7 @@ zephcore/ │ ├── gps/ZephyrGPSManager.cpp/h # GNSS state machine, fix validation, public API │ ├── gps/gps_power.cpp, gps_module_cfg.cpp, gps_internal.h # module power; module configuration + diag │ ├── mqtt/ZephyrMQTTPublisher.c/h # MQTT packet publisher (observer / uplink) +│ ├── mqtt/uplink_creds.h # WiFi/MQTT/location credentials struct (persisted by app/observer_creds) │ ├── ota/wifi_ota.c/h # WiFi SoftAP + HTTP firmware upload │ ├── rng/ZephyrRNG.cpp/h # Hardware CSPRNG with PRNG fallback │ ├── sensors/ # ZephyrSensorManager (upstream SensorManager) over the I2C env sensors + power monitors @@ -1271,7 +1272,10 @@ cleared register. nRF implements `hwinfo_clear_reset_cause()`; ESP32 does not, so the weak stub returns `-ENOSYS` and nothing is cleared there. That is harmless on ESP32, whose cause comes from `esp_reset_reason()` and does not accumulate across boots, but the capture is what readers should -rely on rather than the register. +rely on rather than the register. Zephyr's ESP32 hwinfo driver returns 0 for +the reasons it has no case for; `boot_info` maps those itself (USB-Serial-JTAG +and JTAG resets, i.e. every esptool flash, to `DEBUG`; power glitch to +`BROWNOUT`; CPU lockup; eFuse error to `HARDWARE`). Everything below is software: bounded stall detection in the layer that owns the state machine. Each entry names what it recovers, because several are diff --git a/docs/DESIGN.md b/docs/DESIGN.md index dabf6ae..0680eac 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -185,7 +185,7 @@ flowchart TB | V2 | `helpers/CommonCLI.cpp` includes the GPS manager, `ZephyrBoard`, WiFi OTA and UI headers (it serves `gps sync`, `start dfu`, `start ota`, display and input rotation). Sensors and GPS settings go through upstream's `SensorManager` since 2026-09-25. | Accepted: the remaining includes serve ZephCore-only commands. | | V3 | `helpers/boot_prefs.h` includes the GPS manager (applies GPS prefs at boot, as upstream's `applyGpsPrefs()`). | Accepted. | | V4 | The joystick UI calls `CompanionMesh` directly. | **Accepted by decision**: it runs on the main thread, so the calls are safe, and upstream's UITask reads `the_mesh` the same way. One access point (`JoystickUITask::getMesh()`). | -| V5 | The MQTT adapter includes `app/observer_creds.h`. | Open, small. | +| V5 | The MQTT adapter included `app/observer_creds.h`. | **Resolved 2026-09-26**: the credentials struct lives in `adapters/mqtt/uplink_creds.h`; the roles keep its persistence. | ### 5.4 Component inventory @@ -358,6 +358,6 @@ upstream alignment (§9). 1. `CompanionMesh.cpp` (~3.5k lines) and `CommonCLI.cpp` (~2k) remain the largest files, though now in upstream's shape (one handler per opcode; upstream command layout). -2. V5 (§5.3). -3. Merging the GPS standby and shutdown pin sequences needs current measurements first. -4. ESP32 reset cause is not reported (`get pwrmgt.bootreason` shows `Unknown`). + +None beyond that: the items left open at the end of the restructure (V5, the GPS off sequences, the ESP32 reset +cause) were closed on 2026-09-26. diff --git a/zephcore/adapters/gps/gps_power.cpp b/zephcore/adapters/gps/gps_power.cpp index fb04a8c..6bdd54a 100644 --- a/zephcore/adapters/gps/gps_power.cpp +++ b/zephcore/adapters/gps/gps_power.cpp @@ -151,6 +151,58 @@ static const struct gpio_dt_spec gps_resetb_gpio = GPIO_DT_SPEC_GET(DT_ALIAS(gps static bool gps_gpio_configured = false; #endif +/* The off levels, shared by gps_power_control(false) and System OFF: reset + * and resetb asserted (the module held in reset while its supply is gated, + * so it starts cleanly on the next power-on), the enable and rtcint + * de-asserted, VRTC de-asserted unless keep_vrtc (warm standby). + * + * The sleep line is the one difference: runtime standby/off leaves it + * asserted, System OFF drops it -- as upstream, whose T1000-E sleep_gps() and + * stop_gps() drive GPS_SLEEP_INT HIGH and only powerOff() drives it LOW. + * + * gpio_pin_configure_dt() on every line: a boot with the GPS off never ran + * the power-on path, so the pins may not be outputs yet. */ +static void gps_drive_off_levels(bool keep_vrtc, bool system_off) +{ +#if HAS_GPS_RESET + if (gpio_is_ready_dt(&gps_reset_gpio)) { + gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); + } +#endif +#if HAS_GPS_VRTC + if (!keep_vrtc && gpio_is_ready_dt(&gps_vrtc_gpio)) { + gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE); + } +#else + ARG_UNUSED(keep_vrtc); +#endif +#if HAS_GPS_POWER_CONTROL + /* INACTIVE, not LOW: physical LOW leaves an active-low enable asserted. */ + if (gpio_is_ready_dt(&gps_enable_gpio)) { + gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE); + gps_gpio_configured = true; + } +#endif +#if HAS_GPS_SLEEP + if (system_off && gpio_is_ready_dt(&gps_sleep_gpio)) { + gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_INACTIVE); + } +#else + ARG_UNUSED(system_off); +#endif +#if HAS_GPS_RTCINT + if (gpio_is_ready_dt(&gps_rtcint_gpio)) { + gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); + } +#endif +#if HAS_GPS_RESETB + /* Declared GPIO_ACTIVE_LOW, so ACTIVE drives it LOW. */ + if (gpio_is_ready_dt(&gps_resetb_gpio)) { + gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_ACTIVE); + } +#endif +} + /* GPS power control with warm standby support. * @param on true = power on, false = power off * @param keep_vrtc When powering off: true = keep VRTC alive (warm standby, @@ -263,62 +315,7 @@ void gps_power_control(bool on, bool keep_vrtc) } #endif } else { - /* Power off sequence */ -#if HAS_GPS_RESET - /* Hold GPS in reset during power-off — matches Arduino sleep_gps()/stop_gps(). - * Ensures chip sees RESET asserted when GPS_EN is asserted on next - * power-on, preventing uncontrolled startup before the reset pulse. - * Configure-on-first-use (mirrors the GPS_EN pin below): on a - * boot-with-GPS-off the power-on path never ran, so the pin isn't an - * output yet — gpio_pin_set_dt() alone would leave it floating instead - * of asserting reset. GPIO_OUTPUT_ACTIVE drives the active (asserted) - * level directly. */ - if (gpio_is_ready_dt(&gps_reset_gpio)) { - if (!gps_gpio_configured) { - gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); - } else { - gpio_pin_set_dt(&gps_reset_gpio, 1); - } - } -#endif - -#if HAS_GPS_VRTC - if (!keep_vrtc) { - /* Full power-off: VRTC off too (cold start on next wake) */ - if (gpio_is_ready_dt(&gps_vrtc_gpio)) { - if (!gps_gpio_configured) { - gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE); - } else { - gpio_pin_set_dt(&gps_vrtc_gpio, 0); - } - } - } - /* else: warm standby — VRTC stays asserted, preserving - * ephemeris/almanac/RTC for fast re-acquisition (~1-2 µA) */ -#endif - if (gpio_is_ready_dt(&gps_enable_gpio)) { - if (!gps_gpio_configured) { - gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE); - gps_gpio_configured = true; - } else { - gpio_pin_set_dt(&gps_enable_gpio, 0); - } - } - -#if HAS_GPS_RESETB - /* Assert RESETB when GPS is off (Arduino sleep_gps/stop_gps). - * The alias is declared GPIO_ACTIVE_LOW, so this drives it LOW. */ - if (gpio_is_ready_dt(&gps_resetb_gpio)) { - gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_ACTIVE); - } -#endif - -#if HAS_GPS_RTCINT - /* GPS_RTC_INT stays de-asserted during sleep/off (same as normal operation) */ - if (gpio_is_ready_dt(&gps_rtcint_gpio)) { - gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif + gps_drive_off_levels(keep_vrtc, false); #if HAS_GPS_VRTC LOG_INF("GPS power OFF (%s)", keep_vrtc ? @@ -332,12 +329,9 @@ void gps_power_control(bool on, bool keep_vrtc) #endif } -/* Put every GPS control line this board declares into its System OFF state: - * the power enable, and where present VRTC, sleep and rtcint, de-asserted; - * reset and resetb asserted, holding the module in reset while its supply is - * gated -- the same levels gps_power_control(false) leaves them at. - * Uses gpio_pin_configure_dt() so pins are properly set even if - * gps_power_control() was never called (GPIO not yet configured). */ +/* Put every GPS control line this board declares into its System OFF state + * (see gps_drive_off_levels()), the regulator rail off and VRTC off too. + * Configures the pins, so it works even if gps_power_control() never ran. */ void gps_power_off_for_shutdown(void) { #if HAS_GPS_POWER_REGULATOR @@ -346,40 +340,7 @@ void gps_power_off_for_shutdown(void) gps_reg_enabled = false; } #endif -#if HAS_GPS_POWER_CONTROL - /* INACTIVE: physical LOW leaves an active-low enable asserted, i.e. the - * module still powered across System OFF. */ - if (gpio_is_ready_dt(&gps_enable_gpio)) { - gpio_pin_configure_dt(&gps_enable_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif -#if HAS_GPS_VRTC - if (gpio_is_ready_dt(&gps_vrtc_gpio)) { - gpio_pin_configure_dt(&gps_vrtc_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif -#if HAS_GPS_RESET - /* ACTIVE, matching the power-off path: reset stays asserted across - * System OFF, as Arduino stop_gps() leaves it. */ - if (gpio_is_ready_dt(&gps_reset_gpio)) { - gpio_pin_configure_dt(&gps_reset_gpio, GPIO_OUTPUT_ACTIVE); - } -#endif -#if HAS_GPS_SLEEP - if (gpio_is_ready_dt(&gps_sleep_gpio)) { - gpio_pin_configure_dt(&gps_sleep_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif -#if HAS_GPS_RTCINT - if (gpio_is_ready_dt(&gps_rtcint_gpio)) { - gpio_pin_configure_dt(&gps_rtcint_gpio, GPIO_OUTPUT_INACTIVE); - } -#endif -#if HAS_GPS_RESETB - if (gpio_is_ready_dt(&gps_resetb_gpio)) { - gpio_pin_configure_dt(&gps_resetb_gpio, GPIO_OUTPUT_ACTIVE); - } -#endif + gps_drive_off_levels(false, true); } #if HAS_GNSS diff --git a/zephcore/adapters/mqtt/ZephyrMQTTPublisher.c b/zephcore/adapters/mqtt/ZephyrMQTTPublisher.c index 8408e01..79721c8 100644 --- a/zephcore/adapters/mqtt/ZephyrMQTTPublisher.c +++ b/zephcore/adapters/mqtt/ZephyrMQTTPublisher.c @@ -13,7 +13,7 @@ #include "ZephyrMQTTPublisher.h" #include "ZephyrWiFiStation.h" /* g_wifi_events, WIFI_READY_BIT, zc_wifi_station_* */ -#include "observer_creds.h" +#include "uplink_creds.h" #include #include diff --git a/zephcore/adapters/mqtt/uplink_creds.h b/zephcore/adapters/mqtt/uplink_creds.h new file mode 100644 index 0000000..a908c23 --- /dev/null +++ b/zephcore/adapters/mqtt/uplink_creds.h @@ -0,0 +1,42 @@ +/* + * SPDX-License-Identifier: MIT + * Uplink credentials: WiFi, MQTT broker and location for the observer and the + * repeater uplink. Read by the WiFi station and the MQTT publisher; loaded, + * saved and edited by the roles (app/observer_creds.h). + * + * The struct is persisted as raw bytes (/lfs/repeater/obs_creds): keep the + * field order and sizes. + */ + +#pragma once + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +struct ObserverCreds { + char wifi_ssid[64]; /* WiFi network name */ + char wifi_psk[64]; /* WiFi password (empty = open network) */ + char mqtt_host[128]; /* MQTT broker hostname, e.g. "abydos.hu" */ + uint16_t mqtt_port; /* MQTT broker port, e.g. 8883 */ + uint8_t mqtt_tls; /* 1 = TLS (no cert verify), 0 = plaintext */ + char mqtt_user[64]; /* MQTT username */ + char mqtt_password[64]; /* MQTT password */ + char mqtt_iata[8]; /* IATA location code, e.g. "BUD", "BTS", "VIE" */ + uint8_t _reserved[1]; /* alignment / future use */ + int32_t lat_e6; /* latitude × 1 000 000 (e.g. 47497900 = 47.497900°N) */ + int32_t lon_e6; /* longitude × 1 000 000 (e.g. 19040200 = 19.040200°E) */ +}; + +/* Apply sensible defaults to a freshly-zeroed creds struct. */ +static inline void observer_creds_init(struct ObserverCreds *creds) +{ + creds->mqtt_port = 8883; + creds->mqtt_tls = 1; +} + +#ifdef __cplusplus +} +#endif diff --git a/zephcore/app/observer_creds.h b/zephcore/app/observer_creds.h index c2f4dfa..0740159 100644 --- a/zephcore/app/observer_creds.h +++ b/zephcore/app/observer_creds.h @@ -11,24 +11,12 @@ #include #include +#include "../adapters/mqtt/uplink_creds.h" /* struct ObserverCreds, observer_creds_init() */ + #ifdef __cplusplus extern "C" { #endif -struct ObserverCreds { - char wifi_ssid[64]; /* WiFi network name */ - char wifi_psk[64]; /* WiFi password (empty = open network) */ - char mqtt_host[128]; /* MQTT broker hostname, e.g. "abydos.hu" */ - uint16_t mqtt_port; /* MQTT broker port, e.g. 8883 */ - uint8_t mqtt_tls; /* 1 = TLS (no cert verify), 0 = plaintext */ - char mqtt_user[64]; /* MQTT username */ - char mqtt_password[64]; /* MQTT password */ - char mqtt_iata[8]; /* IATA location code, e.g. "BUD", "BTS", "VIE" */ - uint8_t _reserved[1]; /* alignment / future use */ - int32_t lat_e6; /* latitude × 1 000 000 (e.g. 47497900 = 47.497900°N) */ - int32_t lon_e6; /* longitude × 1 000 000 (e.g. 19040200 = 19.040200°E) */ -}; - /* Load creds from /lfs/repeater/obs_creds. * Returns true on success; on failure fills struct with safe zero defaults. */ bool observer_creds_load(struct ObserverCreds *creds, const char *base_path); @@ -36,13 +24,6 @@ bool observer_creds_load(struct ObserverCreds *creds, const char *base_path); /* Save creds to /lfs/repeater/obs_creds. Returns true on success. */ bool observer_creds_save(const struct ObserverCreds *creds, const char *base_path); -/* Apply sensible defaults to a freshly-zeroed creds struct. */ -static inline void observer_creds_init(struct ObserverCreds *creds) -{ - creds->mqtt_port = 8883; - creds->mqtt_tls = 1; -} - #ifdef __cplusplus } #endif diff --git a/zephcore/helpers/boot_info.c b/zephcore/helpers/boot_info.c index 12a071f..0f92204 100644 --- a/zephcore/helpers/boot_info.c +++ b/zephcore/helpers/boot_info.c @@ -12,11 +12,38 @@ #include +#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) +#include +#endif + LOG_MODULE_REGISTER(zephcore_boot, CONFIG_ZEPHCORE_MAIN_LOG_LEVEL); static uint32_t s_reset_cause; static bool s_reset_cause_valid; +#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) +/* Zephyr's ESP32 hwinfo driver reports 0 for the reasons it has no case for, + * the commonest being ESP_RST_USB: esptool's reset over USB-Serial-JTAG, i.e. + * every flash. Map those onto the nearest RESET_* bit so they are labelled + * instead of "Unknown". */ +static uint32_t esp_unmapped_reset_cause(void) +{ + switch (esp_reset_reason()) { + case ESP_RST_USB: + case ESP_RST_JTAG: + return RESET_DEBUG; + case ESP_RST_PWR_GLITCH: + return RESET_BROWNOUT; + case ESP_RST_CPU_LOCKUP: + return RESET_CPU_LOCKUP; + case ESP_RST_EFUSE: + return RESET_HARDWARE; + default: + return 0; + } +} +#endif + /* POST_KERNEL: before main() on every role. */ static int boot_info_init(void) { @@ -27,6 +54,12 @@ static int boot_info_init(void) return 0; } +#if defined(CONFIG_SOC_FAMILY_ESPRESSIF_ESP32) + if (cause == 0) { + cause = esp_unmapped_reset_cause(); + } +#endif + s_reset_cause = cause; s_reset_cause_valid = true; diff --git a/zephcore/helpers/ui/doom_game.c b/zephcore/helpers/ui/doom_game.c index 1042c49..3f01742 100644 --- a/zephcore/helpers/ui/doom_game.c +++ b/zephcore/helpers/ui/doom_game.c @@ -606,8 +606,8 @@ static const uint16_t spr_portal_b[16] = { /* * Fireball — a solid ball confined to the middle 8 rows of the 16×16 cell, so - * it renders at roughly half an enemy's apparent size without the shared - * sprite blitter needing a scale argument. Solid, unlike the outlined portal + * it renders at eye level, at roughly half an enemy's apparent size, through + * the shared blitter at scale 100. Solid, unlike the outlined portal * diamond, so the two never read as the same thing. */ static const uint16_t spr_fireball[16] = { @@ -663,14 +663,41 @@ static const uint16_t *get_sprite_16(const struct doom_enemy *e) /* ========== RAYCASTER ========== */ +/* The sprite texel under screen pixel (sc, sr) of a sprite drawn at + * [x0, x0 + w) x [y0, y0 + h). */ +static inline bool sprite_texel_on(const uint16_t *sprite, int sc, int sr, + int x0, int y0, int w, int h) +{ + int tex_x = (sc - x0) * 16 / w; + int tex_y = (sr - y0) * 16 / h; + + if (tex_x > 15) tex_x = 15; + if (tex_y > 15) tex_y = 15; + return (sprite[tex_y] & (0x8000 >> tex_x)) != 0; +} + +static inline void clear_px(int x, int y) +{ + render_fb[(y / 8) * SCREEN_W + x] &= (uint8_t)~(1U << (y & 7)); +} + /* - * Render one 16×16 sprite at world position (wx, wy). - * Shared by both the portal and all enemy sprites. + * Render one 16×16 sprite at world position (wx, wy), scale_pct percent of a + * wall's height with its feet on the floor. Shared by the portal, the enemies + * and the fireballs. + * + * Sprites are solid with a one-pixel black halo, not distance-dithered like + * the walls. On a 1-bit panel a dithered sprite ORed over the dithered floor + * band at the horizon has nothing to contrast against: the L2 boss, which + * starts 8-12 tiles away, was a few dots at best and a grey smudge in the + * floor's own pattern at 4 tiles. The halo pass clears first, then the fill + * sets; both honour the zbuffer, so a nearer wall is never cut into. */ static void render_one_sprite(const uint16_t *sprite, fixed_t wx, fixed_t wy, fixed_t inv_det, - struct doom_player *p) + struct doom_player *p, + int scale_pct) { fixed_t sx = wx - p->x; fixed_t sy = wy - p->y; @@ -686,46 +713,49 @@ static void render_one_sprite(const uint16_t *sprite, fp_to_int(fp_div(fp_mul(transform_x, fp_from_int(SCREEN_W)), transform_y)); - int sprite_h = fp_to_int(fp_abs(fp_div( + int base_h = fp_to_int(fp_abs(fp_div( fp_from_int(SCREEN_H), transform_y))); - if (sprite_h < 3) return; - if (sprite_h > SCREEN_H * 2) sprite_h = SCREEN_H * 2; + if (base_h > SCREEN_H * 2) base_h = SCREEN_H * 2; - int draw_start_y = (SCREEN_H - sprite_h) / 2; - int draw_end_y = draw_start_y + sprite_h; + int sprite_h = base_h * scale_pct / 100; + if (sprite_h < 3) return; + + int draw_end_y = (SCREEN_H + base_h) / 2; /* the floor line */ + int draw_start_y = draw_end_y - sprite_h; int draw_start_x = sprite_screen_x - sprite_h / 2; int draw_end_x = sprite_screen_x + sprite_h / 2; + int sprite_w = draw_end_x - draw_start_x; - /* Floor for sprites: at 8+ tiles the wall fade (brightness 1) passes one - * dither cell in 16, so the boss, which starts ~11 tiles away in a - * corner, drew as a few dots or nothing while its fireballs hit. Walls - * keep the full fade; sprites keep a silhouette. */ - int brightness = dist_to_brightness(transform_y); - if (brightness < 7) { - brightness = 7; - } + int x_lo = draw_start_x < 0 ? 0 : draw_start_x; + int x_hi = draw_end_x > SCREEN_W ? SCREEN_W : draw_end_x; + int y_lo = draw_start_y < 0 ? 0 : draw_start_y; + int y_hi = draw_end_y > SCREEN_H ? SCREEN_H : draw_end_y; - for (int sc = draw_start_x; sc < draw_end_x; sc++) { - if (sc < 0 || sc >= SCREEN_W) continue; - if (transform_y >= zbuffer[sc]) continue; + for (int pass = 0; pass < 2; pass++) { + for (int sc = x_lo; sc < x_hi; sc++) { + if (transform_y >= zbuffer[sc]) continue; - int tex_x = (sc - draw_start_x) * 16 / sprite_h; - if (tex_x < 0) tex_x = 0; - if (tex_x > 15) tex_x = 15; - - for (int sr = draw_start_y; sr < draw_end_y; sr++) { - if (sr < 0 || sr >= SCREEN_H) continue; - - int tex_y = (sr - draw_start_y) * 16 / sprite_h; - if (tex_y < 0) tex_y = 0; - if (tex_y > 15) tex_y = 15; - - if (sprite[tex_y] & (0x8000 >> tex_x)) { - uint8_t threshold = - bayer4x4[sr & 3][sc & 3]; - if (brightness > threshold) { + for (int sr = y_lo; sr < y_hi; sr++) { + if (!sprite_texel_on(sprite, sc, sr, + draw_start_x, draw_start_y, + sprite_w, sprite_h)) { + continue; + } + if (pass == 1) { render_fb[(sr / 8) * SCREEN_W + sc] |= (1U << (sr & 7)); + continue; + } + for (int hx = sc - 1; hx <= sc + 1; hx++) { + if (hx < 0 || hx >= SCREEN_W || + transform_y >= zbuffer[hx]) { + continue; + } + for (int hy = sr - 1; hy <= sr + 1; hy++) { + if (hy >= 0 && hy < SCREEN_H) { + clear_px(hx, hy); + } + } } } } @@ -748,7 +778,7 @@ static void render_sprites(void) render_one_sprite(pspr, fp_from_int(game.portal_map_x) + FP_HALF, fp_from_int(game.portal_map_y) + FP_HALF, - inv_det, p); + inv_det, p, 100); } /* Collect visible enemies and sort farthest-first */ @@ -781,7 +811,8 @@ static void render_sprites(void) for (int i = 0; i < visible; i++) { struct doom_enemy *e = &game.enemies[order[i].idx]; - render_one_sprite(get_sprite_16(e), e->x, e->y, inv_det, p); + render_one_sprite(get_sprite_16(e), e->x, e->y, inv_det, p, + e->type == ENEMY_BOSS ? 150 : 100); } /* @@ -793,7 +824,7 @@ static void render_sprites(void) for (int i = 0; i < MAX_FIREBALLS; i++) { struct doom_fireball *f = &game.fireballs[i]; if (!f->active) continue; - render_one_sprite(spr_fireball, f->x, f->y, inv_det, p); + render_one_sprite(spr_fireball, f->x, f->y, inv_det, p, 100); } }