Dungeon core

Source: src/dungeon.c · 1708 lines · 70 functions.

Module responsibility

Owns the game state machine, player movement, room events, AI route selection, controls, screen updates and drawing.

See how this module fits into the complete Dungeon system →

Module entry points

These functions are exposed to the rest of the Dungeon system. Open one to follow its decisions and helper calls.

How to read these diagrams

Each function has an expandable structural diagram derived from the current C source. Decisions split into True and False, loop bodies are indented, and arrows show statement order.

These are source diagrams before preprocessing, not runtime traces. Callback execution, task scheduling and macro expansion are not inferred.

Functions in this file

traceDungeonCreation() · source line 84
static void traceDungeonCreation(const char *stage, int error, int errno_value)
  1. Execute(void)stage;
  2. Execute(void)error;
  3. Execute(void)errno_value;
Read original function source
static void traceDungeonCreation(const char *stage, int error, int errno_value)
{
    /* Temporary creation diagnostics retained for future SPIFFS investigation.
    snprintf(creation_trace, sizeof(creation_trace), "%s E%d N%d", stage, error, errno_value);
    ESP_LOGI("dungeon", "CREATE %s error=%d errno=%d", stage, error, errno_value);
    */
    (void)stage;
    (void)error;
    (void)errno_value;
}
startPlayTimer() · source line 95
static void startPlayTimer(uint32_t now_ms)
  1. Executelast_play_time_ms = now_ms;
  2. Executeplay_timer_running = true;
Read original function source
static void startPlayTimer(uint32_t now_ms)
{
    last_play_time_ms = now_ms;
    play_timer_running = true;
}
commitPlayTime() · source line 101
static void commitPlayTime(uint32_t now_ms)

Calls into this documentation:

  1. Decision!play_timer_running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeconst uint32_t elapsed = now_ms - last_play_time_ms;
  3. Executeconst uint32_t elapsed_seconds = elapsed / 1000U;
  4. Decisionelapsed_seconds == 0U
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  5. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  6. Executeplayer->timePlayedSeconds = UINT32_MAX - player->timePlayedSeconds < elapsed_seconds ? UINT32_MAX : player->timePlayedSeconds + elapsed_seconds;
  7. Executelast_play_time_ms += elapsed_seconds * 1000U;
Read original function source
static void commitPlayTime(uint32_t now_ms)
{
    if (!play_timer_running) return;
    const uint32_t elapsed = now_ms - last_play_time_ms;
    const uint32_t elapsed_seconds = elapsed / 1000U;
    if (elapsed_seconds == 0U) return;
    DungeonPlayerState *player = getDungeonPlayerState();
    player->timePlayedSeconds = UINT32_MAX - player->timePlayedSeconds < elapsed_seconds
        ? UINT32_MAX : player->timePlayedSeconds + elapsed_seconds;
    last_play_time_ms += elapsed_seconds * 1000U;
}
prepareEntranceImage() · source line 116
static bool prepareEntranceImage(void)

Calls into this documentation:

  1. ExecuteDungeonWorld *world = getDungeonWorld();
  2. Executeconst DungeonCell *cell = &world->floors[world->entrance.floor].cells[world->entrance.y][world->entrance.x];
  3. Exit this functionreturn resolveDungeonTilePath(cell, tile_path, sizeof(tile_path));
Read original function source
static bool prepareEntranceImage(void)
{
    DungeonWorld *world = getDungeonWorld();
    const DungeonCell *cell = &world->floors[world->entrance.floor].cells[world->entrance.y][world->entrance.x];
    return resolveDungeonTilePath(cell, tile_path, sizeof(tile_path));
}
prepareStartBackground() · source line 123
static void prepareStartBackground(void)
  1. Executeconst int written = snprintf(tile_path, sizeof(tile_path), "%s", DUNGEON_START_BACKGROUND_PATH);
  2. Executeimage_visible = false;
  3. Executedecode_pending = written > 0 && (size_t)written < sizeof(tile_path);
Read original function source
static void prepareStartBackground(void)
{
    const int written = snprintf(tile_path, sizeof(tile_path), "%s", DUNGEON_START_BACKGROUND_PATH);
    image_visible = false;
    decode_pending = written > 0 && (size_t)written < sizeof(tile_path);
}
prepareRoomImage() · source line 130
static bool prepareRoomImage(const DungeonPosition *position)

Calls into this documentation:

  1. Executeconst DungeonCell *cell = &getDungeonWorld()->floors[position->floor].cells[position->y][position->x];
  2. Exit this functionreturn resolveDungeonTilePath(cell, tile_path, sizeof(tile_path));
Read original function source
static bool prepareRoomImage(const DungeonPosition *position)
{
    const DungeonCell *cell = &getDungeonWorld()->floors[position->floor].cells[position->y][position->x];
    return resolveDungeonTilePath(cell, tile_path, sizeof(tile_path));
}
recordExplored() · source line 136
static void recordExplored(const DungeonPosition *position)

Calls into this documentation:

  1. ExecutemarkDungeonExplored(position, &explored_result);
Read original function source
static void recordExplored(const DungeonPosition *position)
{
    markDungeonExplored(position, &explored_result);
}
isEncounterRoom() · source line 141
static bool isEncounterRoom(const DungeonCell *cell)
  1. Exit this functionreturn cell->tileType == DUNGEON_TILE_ROOM_OPEN || cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_ROUND || cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_SQUARE || cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT || cell->tileType == DUNGEON_TILE_EXIT;
Read original function source
static bool isEncounterRoom(const DungeonCell *cell)
{
    return cell->tileType == DUNGEON_TILE_ROOM_OPEN || cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_ROUND ||
           cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_SQUARE || cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT ||
           cell->tileType == DUNGEON_TILE_EXIT;
}
isShopRoom() · source line 148
static bool isShopRoom(const DungeonCell *cell)
  1. Exit this functionreturn cell->tileType == DUNGEON_TILE_STAIRS_UP || cell->tileType == DUNGEON_TILE_STAIRS_DOWN || cell->tileType == DUNGEON_TILE_STAIRS_BOTH;
Read original function source
static bool isShopRoom(const DungeonCell *cell)
{
    return cell->tileType == DUNGEON_TILE_STAIRS_UP || cell->tileType == DUNGEON_TILE_STAIRS_DOWN || cell->tileType == DUNGEON_TILE_STAIRS_BOTH;
}
applyMoveRecovery() · source line 153
static void applyMoveRecovery(DungeonPlayerState *player)

Calls into this documentation:

  1. Executeconst DungeonConfig *config = getDungeonConfig();
  2. Decisionconfig->healthRegenMoves == 0U || player->moves % config->healthRegenMoves != 0U
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Executeconst uint16_t limit = (uint16_t)(((uint32_t)player->maxHealth * config->healthRegenLimitPercent + 99U) / 100U);
  4. Decisionplayer->health < limit
    True
      1. Executeconst uint32_t recovered = (uint32_t)player->health + config->healthRegenAmount;
      2. Executeplayer->health = recovered > limit ? limit : (uint16_t)recovered;
    False

    Continue after this decision.

Read original function source
static void applyMoveRecovery(DungeonPlayerState *player)
{
    const DungeonConfig *config = getDungeonConfig();
    if (config->healthRegenMoves == 0U || player->moves % config->healthRegenMoves != 0U) return;
    const uint16_t limit = (uint16_t)(((uint32_t)player->maxHealth * config->healthRegenLimitPercent + 99U) / 100U);
    if (player->health < limit) {
        const uint32_t recovered = (uint32_t)player->health + config->healthRegenAmount;
        player->health = recovered > limit ? limit : (uint16_t)recovered;
    }
}
encounterEnemyType() · source line 164
static uint8_t encounterEnemyType(const DungeonCell *cell)

Calls into this documentation:

  1. Executeconst uint8_t saved_type = (uint8_t)((cell->eventFlags & DUNGEON_ENEMY_DETAIL_MASK) >> DUNGEON_ENEMY_DETAIL_SHIFT);
  2. Decisionsaved_type != DUNGEON_ENEMY_NONE
    True
    1. Exit this functionreturn saved_type;
    False

    Continue after this decision.

  3. Executeconst DungeonConfig *config = getDungeonConfig();
  4. Exit this functionreturn player_position.floor == 0U ? config->floor1EnemyMinLevel : player_position.floor == 1U ? config->floor2EnemyMinLevel : config->floor3EnemyMinLevel;
Read original function source
static uint8_t encounterEnemyType(const DungeonCell *cell)
{
    const uint8_t saved_type = (uint8_t)((cell->eventFlags & DUNGEON_ENEMY_DETAIL_MASK) >> DUNGEON_ENEMY_DETAIL_SHIFT);
    if (saved_type != DUNGEON_ENEMY_NONE) return saved_type;
    const DungeonConfig *config = getDungeonConfig();
    return player_position.floor == 0U ? config->floor1EnemyMinLevel : player_position.floor == 1U ? config->floor2EnemyMinLevel : config->floor3EnemyMinLevel;
}
prepareEventImage() · source line 175
static void prepareEventImage(const char *prefix, uint8_t variant)
  1. Executeconst unsigned image_variant = variant == 0U ? 1U : variant;
  2. Executeconst int written = snprintf(tile_path, sizeof(tile_path), DUNGEON_TILE_ROOT "/t_m/%s%02u.jpg", prefix, image_variant);
  3. Executeimage_visible = false;
  4. Executedecode_pending = written > 0 && (size_t)written < sizeof(tile_path);
Read original function source
static void prepareEventImage(const char *prefix, uint8_t variant)
{
    const unsigned image_variant = variant == 0U ? 1U : variant;
    const int written = snprintf(tile_path, sizeof(tile_path), DUNGEON_TILE_ROOT "/t_m/%s%02u.jpg", prefix, image_variant);
    image_visible = false;
    decode_pending = written > 0 && (size_t)written < sizeof(tile_path);
}
prepareRewardImage() · source line 183
static void prepareRewardImage(const DungeonCell *cell, DungeonTreasureType treasure)

Calls into this documentation:

  1. Executeconst char *prefix = reward_is_key ? "t_key_" : treasure == DUNGEON_TREASURE_HEALTH ? "t_health_" : treasure == DUNGEON_TREASURE_LIFE ? "t_life_" : treasure == DUNGEON_TREASURE_ATTACK ? "t_att_" : treasure == DUNGEON_TREASURE_DEFENCE ? "t_def_" : "t_gold_";
  2. ExecuteprepareEventImage(prefix, cell->variant);
Read original function source
static void prepareRewardImage(const DungeonCell *cell, DungeonTreasureType treasure)
{
    const char *prefix = reward_is_key ? "t_key_" : treasure == DUNGEON_TREASURE_HEALTH ? "t_health_" :
                         treasure == DUNGEON_TREASURE_LIFE ? "t_life_" : treasure == DUNGEON_TREASURE_ATTACK ? "t_att_" :
                         treasure == DUNGEON_TREASURE_DEFENCE ? "t_def_" : "t_gold_";
    prepareEventImage(prefix, cell->variant);
}
resolveRoomTrap() · source line 194
static bool resolveRoomTrap(void)

Calls into this documentation:

  1. ExecuteDungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
  2. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  3. Executeconst DungeonPlayerState previous_player = *player;
  4. Executeconst uint16_t previous_flags = cell->stateFlags;
  5. Executeconst DungeonConfig *config = getDungeonConfig();
  6. Executeuint16_t minimum = player_position.floor == 0U ? config->floor1TrapDamageMin : player_position.floor == 1U ? config->floor2TrapDamageMin : config->floor3TrapDamageMin;
  7. Executeuint16_t maximum = player_position.floor == 0U ? config->floor1TrapDamageMax : player_position.floor == 1U ? config->floor2TrapDamageMax : config->floor3TrapDamageMax;
  8. Executeconst uint32_t rolled_damage = minimum + esp_random() % ((uint32_t)maximum - minimum + 1U);
  9. Executeconst uint32_t reduced_damage = rolled_damage > player->defence ? rolled_damage - player->defence : config->combatMinimumDamage;
  10. Executetrap_damage = (uint16_t)(reduced_damage < config->combatMinimumDamage ? config->combatMinimumDamage : reduced_damage);
  11. Executeplayer->health = trap_damage >= player->health ? 0U : (uint16_t)(player->health - trap_damage);
  12. Executecell->stateFlags |= DUNGEON_STATE_TRIGGERED;
  13. Decisionplayer->health == 0U
    True
      1. ExecuteDungeonPosition respawn = getDungeonWorld()->entrance;
      2. Decisionplayer_position.floor > 0U
        True
        1. Executerespawn = getDungeonWorld()->stairLinks[player_position.floor - 1U].lower;
        False

        Continue after this decision.

      3. Executeplayer->position = respawn;
      4. Executeplayer->health = (uint16_t)(player->maxHealth / 2U + player->maxHealth % 2U);
      5. Executeplayer_position = respawn;
      6. Executetrap_defeated = true;
    False
    1. Executetrap_defeated = false;
  14. Decision!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)
    True
      1. Execute*player = previous_player;
      2. Executeplayer_position = previous_player.position;
      3. Executecell->stateFlags = previous_flags;
      4. ExecutesaveDungeonWorld(&storage_result);
      5. Exit this functionreturn false;
    False

    Continue after this decision.

  15. ExecuterecordExplored(&player_position);
  16. Executestate = DUNGEON_TRAP_RESULT;
  17. ExecuteprepareEventImage("t_trap_", cell->variant);
  18. Exit this functionreturn true;
Read original function source
static bool resolveRoomTrap(void)
{
    DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    const uint16_t previous_flags = cell->stateFlags;
    const DungeonConfig *config = getDungeonConfig();
    uint16_t minimum = player_position.floor == 0U ? config->floor1TrapDamageMin : player_position.floor == 1U ? config->floor2TrapDamageMin : config->floor3TrapDamageMin;
    uint16_t maximum = player_position.floor == 0U ? config->floor1TrapDamageMax : player_position.floor == 1U ? config->floor2TrapDamageMax : config->floor3TrapDamageMax;
    const uint32_t rolled_damage = minimum + esp_random() % ((uint32_t)maximum - minimum + 1U);
    const uint32_t reduced_damage = rolled_damage > player->defence ? rolled_damage - player->defence : config->combatMinimumDamage;
    trap_damage = (uint16_t)(reduced_damage < config->combatMinimumDamage ? config->combatMinimumDamage : reduced_damage);
    player->health = trap_damage >= player->health ? 0U : (uint16_t)(player->health - trap_damage);
    cell->stateFlags |= DUNGEON_STATE_TRIGGERED;
    if (player->health == 0U) {
        DungeonPosition respawn = getDungeonWorld()->entrance;
        if (player_position.floor > 0U) respawn = getDungeonWorld()->stairLinks[player_position.floor - 1U].lower;
        player->position = respawn;
        player->health = (uint16_t)(player->maxHealth / 2U + player->maxHealth % 2U);
        player_position = respawn;
        trap_defeated = true;
    } else trap_defeated = false;
    if (!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        player_position = previous_player.position;
        cell->stateFlags = previous_flags;
        saveDungeonWorld(&storage_result);
        return false;
    }
    recordExplored(&player_position);
    state = DUNGEON_TRAP_RESULT;
    prepareEventImage("t_trap_", cell->variant);
    return true;
}
startRoomEvent() · source line 229
static bool startRoomEvent(const DungeonPosition *source)

Calls into this documentation:

  1. Executeconst DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
  2. Decision(cell->eventFlags & DUNGEON_EVENT_TRAP) != 0U && (cell->stateFlags & DUNGEON_STATE_TRIGGERED) == 0U
    True
    1. Exit this functionreturn resolveRoomTrap();
    False

    Continue after this decision.

  3. Decision(cell->eventFlags & DUNGEON_EVENT_REQUIRES_KEY) != 0U && (cell->stateFlags & DUNGEON_STATE_UNLOCKED) == 0U && getDungeonPlayerState()->keys == 0U
    True
      1. DecisionencounterEnemyType(cell) == DUNGEON_ENEMY_DRAGON
        True
          1. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
          2. Executeconst uint32_t previous_flags = player->runFlags;
          3. Executeplayer->runFlags |= DUNGEON_RUN_KEY_REVEALED;
          4. Decision!saveDungeonPlayer(&player_result)
            True
              1. Executeplayer->runFlags = previous_flags;
              2. Executestate = DUNGEON_NOTICE;
              3. Exit this functionreturn true;
            False

            Continue after this decision.

        False

        Continue after this decision.

      2. Executeencounter_source = *source;
      3. Executestate = DUNGEON_LOCKED_DOOR;
      4. Executeimage_visible = false;
      5. Executedecode_pending = false;
      6. Exit this functionreturn true;
    False

    Continue after this decision.

  4. DecisionisEncounterRoom(cell) && (cell->eventFlags & DUNGEON_EVENT_ENCOUNTER) != 0U && (cell->stateFlags & DUNGEON_STATE_COMPLETED) == 0U
    True
      1. Executeconst uint8_t enemy_type = encounterEnemyType(cell);
      2. Decision!startDungeonCombat(enemy_type, &combat_state)
        True
        1. Exit this functionreturn false;
        False

        Continue after this decision.

      3. Executeencounter_source = *source;
      4. Executeselected_combat_option = COMBAT_ATTACK;
      5. Executecombat_text[0] = '\0';
      6. Executecombat_victory = false;
      7. Executecombat_result_title = NULL;
      8. Executestate = DUNGEON_COMBAT;
      9. ExecuteprepareEventImage(combat_state.enemyType == DUNGEON_ENEMY_LEVEL_1 ? "e_lvl1_" : combat_state.enemyType == DUNGEON_ENEMY_LEVEL_2 ? "e_lvl2_" : combat_state.enemyType == DUNGEON_ENEMY_LEVEL_3 ? "e_lvl3_" : combat_state.enemyType == DUNGEON_ENEMY_LEVEL_4 ? "e_lvl4_" : "e_drag_", cell->variant);
      10. Exit this functionreturn true;
    False

    Continue after this decision.

  5. Exit this functionreturn collectRoomReward();
Read original function source
static bool startRoomEvent(const DungeonPosition *source)
{
    const DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    if ((cell->eventFlags & DUNGEON_EVENT_TRAP) != 0U && (cell->stateFlags & DUNGEON_STATE_TRIGGERED) == 0U) return resolveRoomTrap();
    if ((cell->eventFlags & DUNGEON_EVENT_REQUIRES_KEY) != 0U && (cell->stateFlags & DUNGEON_STATE_UNLOCKED) == 0U && getDungeonPlayerState()->keys == 0U) {
        if (encounterEnemyType(cell) == DUNGEON_ENEMY_DRAGON) {
            DungeonPlayerState *player = getDungeonPlayerState();
            const uint32_t previous_flags = player->runFlags;
            player->runFlags |= DUNGEON_RUN_KEY_REVEALED;
            if (!saveDungeonPlayer(&player_result)) {
                player->runFlags = previous_flags;
                state = DUNGEON_NOTICE;
                return true;
            }
        }
        encounter_source = *source;
        state = DUNGEON_LOCKED_DOOR;
        image_visible = false;
        decode_pending = false;
        return true;
    }
    if (isEncounterRoom(cell) && (cell->eventFlags & DUNGEON_EVENT_ENCOUNTER) != 0U && (cell->stateFlags & DUNGEON_STATE_COMPLETED) == 0U) {
        const uint8_t enemy_type = encounterEnemyType(cell);
        if (!startDungeonCombat(enemy_type, &combat_state)) return false;
        encounter_source = *source;
        selected_combat_option = COMBAT_ATTACK;
        combat_text[0] = '\0';
        combat_victory = false;
        combat_result_title = NULL;
        state = DUNGEON_COMBAT;
        prepareEventImage(combat_state.enemyType == DUNGEON_ENEMY_LEVEL_1 ? "e_lvl1_" : combat_state.enemyType == DUNGEON_ENEMY_LEVEL_2 ? "e_lvl2_" :
                          combat_state.enemyType == DUNGEON_ENEMY_LEVEL_3 ? "e_lvl3_" : combat_state.enemyType == DUNGEON_ENEMY_LEVEL_4 ? "e_lvl4_" : "e_drag_", cell->variant);
        return true;
    }
    return collectRoomReward();
}
collectRoomReward() · source line 266
static bool collectRoomReward(void)

Calls into this documentation:

  1. ExecuteDungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
  2. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  3. Executeconst DungeonPlayerState previous_player = *player;
  4. Executeconst uint16_t previous_flags = cell->stateFlags;
  5. ExecuteDungeonTreasureType treasure = DUNGEON_TREASURE_GOLD;
  6. Executebool pending = false;
  7. Executebool collected = false;
  8. Executereward_text[0] = '\0';
  9. Executereward_is_key = false;
  10. Decision(cell->eventFlags & DUNGEON_EVENT_KEY) != 0U && (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) == 0U
    True
      1. Decisionplayer->keys < UINT16_MAX
        True
        1. Execute++player->keys;
        False

        Continue after this decision.

      2. Executecell->stateFlags |= DUNGEON_STATE_KEY_COLLECTED;
      3. Executesnprintf(reward_text, sizeof(reward_text), "KEY COLLECTED");
      4. Executereward_is_key = true;
      5. Executepending = true;
      6. Executecollected = true;
    False
    1. Decision(cell->eventFlags & DUNGEON_EVENT_TREASURE) != 0U && (cell->stateFlags & DUNGEON_STATE_OPENED) == 0U
      True
        1. Executeconst DungeonConfig *config = getDungeonConfig();
        2. Executetreasure = (DungeonTreasureType)((cell->eventFlags & DUNGEON_TREASURE_DETAIL_MASK) >> DUNGEON_TREASURE_DETAIL_SHIFT);
        3. Executepending = true;
        4. Decisiontreasure == DUNGEON_TREASURE_HEALTH
          True
            1. Decisionplayer->health < player->maxHealth
              True
                1. Executeplayer->health = player->maxHealth;
                2. Executesnprintf(reward_text, sizeof(reward_text), "HEALTH RESTORED");
                3. Executecollected = true;
              False
              1. Decisionplayer->healthPotions < config->maxHealthPotions
                True
                  1. Execute++player->healthPotions;
                  2. Executesnprintf(reward_text, sizeof(reward_text), "POTION +1");
                  3. Executecollected = true;
                False
                1. Executesnprintf(reward_text, sizeof(reward_text), "CANNOT CARRY");
          False
          1. Decisiontreasure == DUNGEON_TREASURE_LIFE
            True
              1. Executeplayer->maxHealth = player->maxHealth <= UINT16_MAX - config->rareLifeHealthGain ? player->maxHealth + config->rareLifeHealthGain : UINT16_MAX;
              2. Executesnprintf(reward_text, sizeof(reward_text), "MAX HEALTH +%u", (unsigned)config->rareLifeHealthGain);
              3. Executecollected = true;
            False
            1. Decisiontreasure == DUNGEON_TREASURE_ATTACK
              True
                1. Executeplayer->attack = player->attack <= UINT16_MAX - config->rareAttackGain ? player->attack + config->rareAttackGain : UINT16_MAX;
                2. Executesnprintf(reward_text, sizeof(reward_text), "ATTACK +%u", (unsigned)config->rareAttackGain);
                3. Executecollected = true;
              False
              1. Decisiontreasure == DUNGEON_TREASURE_DEFENCE
                True
                  1. Executeplayer->defence = player->defence <= UINT16_MAX - config->rareDefenceGain ? player->defence + config->rareDefenceGain : UINT16_MAX;
                  2. Executesnprintf(reward_text, sizeof(reward_text), "DEFENCE +%u", (unsigned)config->rareDefenceGain);
                  3. Executecollected = true;
                False
                  1. Executeuint16_t minimum = player_position.floor == 0U ? config->floor1GoldMin : player_position.floor == 1U ? config->floor2GoldMin : config->floor3GoldMin;
                  2. Executeuint16_t maximum = player_position.floor == 0U ? config->floor1GoldMax : player_position.floor == 1U ? config->floor2GoldMax : config->floor3GoldMax;
                  3. Executeconst uint32_t amount = minimum + (esp_random() % ((uint32_t)maximum - minimum + 1U));
                  4. Executeplayer->gold = player->gold <= UINT32_MAX - amount ? player->gold + amount : UINT32_MAX;
                  5. Executesnprintf(reward_text, sizeof(reward_text), "GOLD +%lu", (unsigned long)amount);
                  6. Executecollected = true;
        5. Decisioncollected
          True
          1. Executecell->stateFlags |= DUNGEON_STATE_OPENED;
          False

          Continue after this decision.

      False

      Continue after this decision.

  11. Decision!pending
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  12. Decision!collected
    True
      1. Executestate = DUNGEON_REWARD;
      2. ExecuteprepareRewardImage(cell, treasure);
      3. Exit this functionreturn true;
    False

    Continue after this decision.

  13. Decision!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)
    True
      1. Execute*player = previous_player;
      2. Executecell->stateFlags = previous_flags;
      3. ExecutesaveDungeonWorld(&storage_result);
      4. Exit this functionreturn false;
    False

    Continue after this decision.

  14. Executestate = DUNGEON_REWARD;
  15. ExecuteprepareRewardImage(cell, treasure);
  16. Exit this functionreturn true;
Read original function source
static bool collectRoomReward(void)
{
    DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    const uint16_t previous_flags = cell->stateFlags;
    DungeonTreasureType treasure = DUNGEON_TREASURE_GOLD;
    bool pending = false;
    bool collected = false;

    reward_text[0] = '\0';
    reward_is_key = false;
    if ((cell->eventFlags & DUNGEON_EVENT_KEY) != 0U && (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) == 0U) {
        if (player->keys < UINT16_MAX) ++player->keys;
        cell->stateFlags |= DUNGEON_STATE_KEY_COLLECTED;
        snprintf(reward_text, sizeof(reward_text), "KEY COLLECTED");
        reward_is_key = true;
        pending = true;
        collected = true;
    } else if ((cell->eventFlags & DUNGEON_EVENT_TREASURE) != 0U && (cell->stateFlags & DUNGEON_STATE_OPENED) == 0U) {
        const DungeonConfig *config = getDungeonConfig();
        treasure = (DungeonTreasureType)((cell->eventFlags & DUNGEON_TREASURE_DETAIL_MASK) >> DUNGEON_TREASURE_DETAIL_SHIFT);
        pending = true;
        if (treasure == DUNGEON_TREASURE_HEALTH) {
            if (player->health < player->maxHealth) {
                player->health = player->maxHealth;
                snprintf(reward_text, sizeof(reward_text), "HEALTH RESTORED");
                collected = true;
            } else if (player->healthPotions < config->maxHealthPotions) {
                ++player->healthPotions;
                snprintf(reward_text, sizeof(reward_text), "POTION +1");
                collected = true;
            } else snprintf(reward_text, sizeof(reward_text), "CANNOT CARRY");
        } else if (treasure == DUNGEON_TREASURE_LIFE) {
            player->maxHealth = player->maxHealth <= UINT16_MAX - config->rareLifeHealthGain ? player->maxHealth + config->rareLifeHealthGain : UINT16_MAX;
            snprintf(reward_text, sizeof(reward_text), "MAX HEALTH +%u", (unsigned)config->rareLifeHealthGain);
            collected = true;
        } else if (treasure == DUNGEON_TREASURE_ATTACK) {
            player->attack = player->attack <= UINT16_MAX - config->rareAttackGain ? player->attack + config->rareAttackGain : UINT16_MAX;
            snprintf(reward_text, sizeof(reward_text), "ATTACK +%u", (unsigned)config->rareAttackGain);
            collected = true;
        } else if (treasure == DUNGEON_TREASURE_DEFENCE) {
            player->defence = player->defence <= UINT16_MAX - config->rareDefenceGain ? player->defence + config->rareDefenceGain : UINT16_MAX;
            snprintf(reward_text, sizeof(reward_text), "DEFENCE +%u", (unsigned)config->rareDefenceGain);
            collected = true;
        } else {
            uint16_t minimum = player_position.floor == 0U ? config->floor1GoldMin : player_position.floor == 1U ? config->floor2GoldMin : config->floor3GoldMin;
            uint16_t maximum = player_position.floor == 0U ? config->floor1GoldMax : player_position.floor == 1U ? config->floor2GoldMax : config->floor3GoldMax;
            const uint32_t amount = minimum + (esp_random() % ((uint32_t)maximum - minimum + 1U));
            player->gold = player->gold <= UINT32_MAX - amount ? player->gold + amount : UINT32_MAX;
            snprintf(reward_text, sizeof(reward_text), "GOLD +%lu", (unsigned long)amount);
            collected = true;
        }
        if (collected) cell->stateFlags |= DUNGEON_STATE_OPENED;
    }

    if (!pending) return false;
    if (!collected) {
        state = DUNGEON_REWARD;
        prepareRewardImage(cell, treasure);
        return true;
    }
    if (!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        cell->stateFlags = previous_flags;
        saveDungeonWorld(&storage_result);
        return false;
    }
    state = DUNGEON_REWARD;
    prepareRewardImage(cell, treasure);
    return true;
}
movePlayer() · source line 339
static bool movePlayer(uint8_t exit)

Calls into this documentation:

  1. Executeconst DungeonCell *current = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
  2. Decision(current->exits & exit) == 0U
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. ExecuteDungeonPosition destination = player_position;
  4. Decisionexit == DUNGEON_EXIT_NORTH && destination.y > 0U
    True
    1. Execute--destination.y;
    False
    1. Decisionexit == DUNGEON_EXIT_EAST && destination.x + 1U < DUNGEON_WIDTH
      True
      1. Execute++destination.x;
      False
      1. Decisionexit == DUNGEON_EXIT_SOUTH && destination.y + 1U < DUNGEON_HEIGHT
        True
        1. Execute++destination.y;
        False
        1. Decisionexit == DUNGEON_EXIT_WEST && destination.x > 0U
          True
          1. Execute--destination.x;
          False
          1. Exit this functionreturn false;
  5. Executeconst DungeonCell *cell = &getDungeonWorld()->floors[destination.floor].cells[destination.y][destination.x];
  6. Decision!resolveDungeonTilePath(cell, tile_path, sizeof(tile_path))
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  7. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  8. Executeconst DungeonPlayerState previous_player = *player;
  9. Executeplayer->position = destination;
  10. Execute++player->moves;
  11. ExecuteapplyMoveRecovery(player);
  12. Decision!saveDungeonPlayer(&player_result)
    True
      1. Execute*player = previous_player;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  13. Executeplayer_position = destination;
  14. ExecuterecordExplored(&player_position);
  15. DecisionstartRoomEvent(&previous_player.position)
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  16. Executeimage_visible = false;
  17. Executedecode_pending = true;
  18. Exit this functionreturn true;
Read original function source
static bool movePlayer(uint8_t exit)
{
    const DungeonCell *current = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    if ((current->exits & exit) == 0U) return false;
    DungeonPosition destination = player_position;
    if (exit == DUNGEON_EXIT_NORTH && destination.y > 0U) --destination.y;
    else if (exit == DUNGEON_EXIT_EAST && destination.x + 1U < DUNGEON_WIDTH) ++destination.x;
    else if (exit == DUNGEON_EXIT_SOUTH && destination.y + 1U < DUNGEON_HEIGHT) ++destination.y;
    else if (exit == DUNGEON_EXIT_WEST && destination.x > 0U) --destination.x;
    else return false;
    const DungeonCell *cell = &getDungeonWorld()->floors[destination.floor].cells[destination.y][destination.x];
    if (!resolveDungeonTilePath(cell, tile_path, sizeof(tile_path))) return false;
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    player->position = destination;
    ++player->moves;
    applyMoveRecovery(player);
    if (!saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        return false;
    }
    player_position = destination;
    recordExplored(&player_position);
    if (startRoomEvent(&previous_player.position)) return true;
    image_visible = false;
    decode_pending = true;
    return true;
}
moveStairs() · source line 371
static bool moveStairs(bool up)

Calls into this documentation:

  1. ExecuteDungeonWorld *world = getDungeonWorld();
  2. Executeconst DungeonCell *current = &world->floors[player_position.floor].cells[player_position.y][player_position.x];
  3. Decision(up && current->tileType != DUNGEON_TILE_STAIRS_UP && current->tileType != DUNGEON_TILE_STAIRS_BOTH) || (!up && current->tileType != DUNGEON_TILE_STAIRS_DOWN && current->tileType != DUNGEON_TILE_STAIRS_BOTH)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  4. ExecuteDungeonPosition destination;
  5. Executebool found = false;
  6. FOR loopunsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst DungeonStairLink *link = &world->stairLinks[index];
      2. Decisionplayer_position.floor == link->upper.floor && player_position.x == link->upper.x && player_position.y == link->upper.y
        True
          1. Executedestination = link->lower;
          2. Executefound = true;
          3. Exit enclosing loop or switchbreak;
        False

        Continue after this decision.

      3. Decisionplayer_position.floor == link->lower.floor && player_position.x == link->lower.x && player_position.y == link->lower.y
        True
          1. Executedestination = link->upper;
          2. Executefound = true;
          3. Exit enclosing loop or switchbreak;
        False

        Continue after this decision.

  7. Decision!found
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  8. Executeconst DungeonCell *cell = &world->floors[destination.floor].cells[destination.y][destination.x];
  9. Decision!resolveDungeonTilePath(cell, tile_path, sizeof(tile_path))
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  10. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  11. Executeconst DungeonPlayerState previous_player = *player;
  12. Executeplayer->position = destination;
  13. Execute++player->moves;
  14. ExecuteapplyMoveRecovery(player);
  15. Decision!saveDungeonPlayer(&player_result)
    True
      1. Execute*player = previous_player;
      2. Exit this functionreturn false;
    False

    Continue after this decision.

  16. Executeplayer_position = destination;
  17. ExecuterecordExplored(&player_position);
  18. DecisionstartRoomEvent(&previous_player.position)
    True
    1. Exit this functionreturn true;
    False

    Continue after this decision.

  19. Executeimage_visible = false;
  20. Executedecode_pending = true;
  21. Exit this functionreturn true;
Read original function source
static bool moveStairs(bool up)
{
    DungeonWorld *world = getDungeonWorld();
    const DungeonCell *current = &world->floors[player_position.floor].cells[player_position.y][player_position.x];
    if ((up && current->tileType != DUNGEON_TILE_STAIRS_UP && current->tileType != DUNGEON_TILE_STAIRS_BOTH) ||
        (!up && current->tileType != DUNGEON_TILE_STAIRS_DOWN && current->tileType != DUNGEON_TILE_STAIRS_BOTH)) return false;
    DungeonPosition destination;
    bool found = false;

    for (unsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
        const DungeonStairLink *link = &world->stairLinks[index];
        if (player_position.floor == link->upper.floor &&
            player_position.x == link->upper.x && player_position.y == link->upper.y) {
            destination = link->lower;
            found = true;
            break;
        }
        if (player_position.floor == link->lower.floor &&
            player_position.x == link->lower.x && player_position.y == link->lower.y) {
            destination = link->upper;
            found = true;
            break;
        }
    }

    if (!found) return false;
    const DungeonCell *cell = &world->floors[destination.floor].cells[destination.y][destination.x];
    if (!resolveDungeonTilePath(cell, tile_path, sizeof(tile_path))) return false;
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    player->position = destination;
    ++player->moves;
    applyMoveRecovery(player);
    if (!saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        return false;
    }
    player_position = destination;
    recordExplored(&player_position);
    if (startRoomEvent(&previous_player.position)) return true;
    image_visible = false;
    decode_pending = true;
    return true;
}
stairAvailable() · source line 416
static bool stairAvailable(bool up)

Calls into this documentation:

  1. ExecuteDungeonWorld *world = getDungeonWorld();
  2. Executeconst DungeonCell *current = &world->floors[player_position.floor].cells[player_position.y][player_position.x];
  3. Decision(up && current->tileType != DUNGEON_TILE_STAIRS_UP && current->tileType != DUNGEON_TILE_STAIRS_BOTH) || (!up && current->tileType != DUNGEON_TILE_STAIRS_DOWN && current->tileType != DUNGEON_TILE_STAIRS_BOTH)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  4. FOR loopunsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst DungeonStairLink *link = &world->stairLinks[index];
      2. Decisionplayer_position.floor == link->upper.floor && player_position.x == link->upper.x && player_position.y == link->upper.y
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

      3. Decisionplayer_position.floor == link->lower.floor && player_position.x == link->lower.x && player_position.y == link->lower.y
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  5. Exit this functionreturn false;
Read original function source
static bool stairAvailable(bool up)
{
    DungeonWorld *world = getDungeonWorld();
    const DungeonCell *current = &world->floors[player_position.floor].cells[player_position.y][player_position.x];
    if ((up && current->tileType != DUNGEON_TILE_STAIRS_UP && current->tileType != DUNGEON_TILE_STAIRS_BOTH) ||
        (!up && current->tileType != DUNGEON_TILE_STAIRS_DOWN && current->tileType != DUNGEON_TILE_STAIRS_BOTH)) return false;
    for (unsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
        const DungeonStairLink *link = &world->stairLinks[index];
        if (player_position.floor == link->upper.floor &&
            player_position.x == link->upper.x && player_position.y == link->upper.y) return true;
        if (player_position.floor == link->lower.floor &&
            player_position.x == link->lower.x && player_position.y == link->lower.y) return true;
    }
    return false;
}
aiPositionIndex() · source line 434
static uint16_t aiPositionIndex(DungeonPosition position)
  1. Exit this functionreturn (uint16_t)(((uint16_t)position.floor * DUNGEON_WIDTH * DUNGEON_HEIGHT) + ((uint16_t)position.y * DUNGEON_WIDTH) + position.x);
Read original function source
static uint16_t aiPositionIndex(DungeonPosition position)
{
    return (uint16_t)(((uint16_t)position.floor * DUNGEON_WIDTH * DUNGEON_HEIGHT) +
                      ((uint16_t)position.y * DUNGEON_WIDTH) + position.x);
}
aiSamePosition() · source line 440
static bool aiSamePosition(DungeonPosition left, DungeonPosition right)
  1. Exit this functionreturn left.floor == right.floor && left.x == right.x && left.y == right.y;
Read original function source
static bool aiSamePosition(DungeonPosition left, DungeonPosition right)
{
    return left.floor == right.floor && left.x == right.x && left.y == right.y;
}
aiNeighbour() · source line 445
static bool aiNeighbour(DungeonPosition position, ai_option_t option, DungeonPosition *destination)

Calls into this documentation:

  1. ExecuteDungeonWorld *world = getDungeonWorld();
  2. Execute*destination = position;
  3. Decisionoption == AI_UP || option == AI_DOWN
    True
      1. FOR loopuint8_t index = 0U; index < DUNGEON_FLOOR_COUNT - 1U; ++index

        Repeat body while the loop condition permits; then continue below.

          1. Executeconst DungeonStairLink *link = &world->stairLinks[index];
          2. Decisionoption == AI_DOWN && aiSamePosition(position, link->upper)
            True
              1. Execute*destination = link->lower;
              2. Exit this functionreturn true;
            False

            Continue after this decision.

          3. Decisionoption == AI_UP && aiSamePosition(position, link->lower)
            True
              1. Execute*destination = link->upper;
              2. Exit this functionreturn true;
            False

            Continue after this decision.

      2. Exit this functionreturn false;
    False

    Continue after this decision.

  4. Executeconst uint8_t exits[] = { DUNGEON_EXIT_NORTH, DUNGEON_EXIT_EAST, DUNGEON_EXIT_SOUTH, DUNGEON_EXIT_WEST };
  5. Executeconst DungeonCell *cell = &world->floors[position.floor].cells[position.y][position.x];
  6. Decision(cell->exits & exits[option]) == 0U
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  7. Decisionoption == AI_NORTH && position.y > 0U
    True
    1. Execute--destination->y;
    False
    1. Decisionoption == AI_EAST && position.x + 1U < DUNGEON_WIDTH
      True
      1. Execute++destination->x;
      False
      1. Decisionoption == AI_SOUTH && position.y + 1U < DUNGEON_HEIGHT
        True
        1. Execute++destination->y;
        False
        1. Decisionoption == AI_WEST && position.x > 0U
          True
          1. Execute--destination->x;
          False
          1. Exit this functionreturn false;
  8. Exit this functionreturn true;
Read original function source
static bool aiNeighbour(DungeonPosition position, ai_option_t option, DungeonPosition *destination)
{
    DungeonWorld *world = getDungeonWorld();
    *destination = position;
    if (option == AI_UP || option == AI_DOWN) {
        for (uint8_t index = 0U; index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
            const DungeonStairLink *link = &world->stairLinks[index];
            if (option == AI_DOWN && aiSamePosition(position, link->upper)) { *destination = link->lower; return true; }
            if (option == AI_UP && aiSamePosition(position, link->lower)) { *destination = link->upper; return true; }
        }
        return false;
    }
    const uint8_t exits[] = { DUNGEON_EXIT_NORTH, DUNGEON_EXIT_EAST, DUNGEON_EXIT_SOUTH, DUNGEON_EXIT_WEST };
    const DungeonCell *cell = &world->floors[position.floor].cells[position.y][position.x];
    if ((cell->exits & exits[option]) == 0U) return false;
    if (option == AI_NORTH && position.y > 0U) --destination->y;
    else if (option == AI_EAST && position.x + 1U < DUNGEON_WIDTH) ++destination->x;
    else if (option == AI_SOUTH && position.y + 1U < DUNGEON_HEIGHT) ++destination->y;
    else if (option == AI_WEST && position.x > 0U) --destination->x;
    else return false;
    return true;
}
aiFindPath() · source line 468
static bool aiFindPath(DungeonPosition target, ai_option_t *first_option)

Calls into this documentation:

  1. Executeconst DungeonPosition start = player_position;
  2. DecisionaiSamePosition(start, target)
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  3. Executememset(ai_route_first_option, 0xFF, sizeof(ai_route_first_option));
  4. Executeuint16_t head = 0U, tail = 0U;
  5. Executeconst uint16_t start_index = aiPositionIndex(start);
  6. Executeai_route_queue[tail++] = start_index;
  7. Executeai_route_first_option[start_index] = 0xFEU;
  8. WHILE loophead < tail

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst uint16_t current_index = ai_route_queue[head++];
      2. ExecuteDungeonPosition current = { (uint8_t)(current_index / (DUNGEON_WIDTH * DUNGEON_HEIGHT)), (uint8_t)(current_index % DUNGEON_WIDTH), (uint8_t)((current_index % (DUNGEON_WIDTH * DUNGEON_HEIGHT)) / DUNGEON_WIDTH) };
      3. FOR loopuint8_t option = AI_NORTH; option <= AI_DOWN; ++option

        Repeat body while the loop condition permits; then continue below.

          1. ExecuteDungeonPosition next;
          2. Decision!aiNeighbour(current, (ai_option_t)option, &next)
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. Executeconst uint16_t next_index = aiPositionIndex(next);
          4. Decisionai_route_first_option[next_index] != 0xFFU
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          5. Executeai_route_first_option[next_index] = current_index == start_index ? option : ai_route_first_option[current_index];
          6. DecisionaiSamePosition(next, target)
            True
              1. Execute*first_option = (ai_option_t)ai_route_first_option[next_index];
              2. Exit this functionreturn true;
            False

            Continue after this decision.

          7. Executeai_route_queue[tail++] = next_index;
  9. Exit this functionreturn false;
Read original function source
static bool aiFindPath(DungeonPosition target, ai_option_t *first_option)
{
    const DungeonPosition start = player_position;
    if (aiSamePosition(start, target)) return false;
    memset(ai_route_first_option, 0xFF, sizeof(ai_route_first_option));
    uint16_t head = 0U, tail = 0U;
    const uint16_t start_index = aiPositionIndex(start);
    ai_route_queue[tail++] = start_index;
    ai_route_first_option[start_index] = 0xFEU;
    while (head < tail) {
        const uint16_t current_index = ai_route_queue[head++];
        DungeonPosition current = { (uint8_t)(current_index / (DUNGEON_WIDTH * DUNGEON_HEIGHT)),
                                    (uint8_t)(current_index % DUNGEON_WIDTH),
                                    (uint8_t)((current_index % (DUNGEON_WIDTH * DUNGEON_HEIGHT)) / DUNGEON_WIDTH) };
        for (uint8_t option = AI_NORTH; option <= AI_DOWN; ++option) {
            DungeonPosition next;
            if (!aiNeighbour(current, (ai_option_t)option, &next)) continue;
            const uint16_t next_index = aiPositionIndex(next);
            if (ai_route_first_option[next_index] != 0xFFU) continue;
            ai_route_first_option[next_index] = current_index == start_index ? option : ai_route_first_option[current_index];
            if (aiSamePosition(next, target)) { *first_option = (ai_option_t)ai_route_first_option[next_index]; return true; }
            ai_route_queue[tail++] = next_index;
        }
    }
    return false;
}
aiNearestUnexplored() · source line 495
static bool aiNearestUnexplored(ai_option_t *first_option)

Calls into this documentation:

  1. Executeconst DungeonPosition start = player_position;
  2. Executememset(ai_route_first_option, 0xFF, sizeof(ai_route_first_option));
  3. Executeuint16_t head = 0U, tail = 0U;
  4. Executeconst uint16_t start_index = aiPositionIndex(start);
  5. Executeai_route_queue[tail++] = start_index;
  6. Executeai_route_first_option[start_index] = 0xFEU;
  7. WHILE loophead < tail

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst uint16_t current_index = ai_route_queue[head++];
      2. ExecuteDungeonPosition current = { (uint8_t)(current_index / (DUNGEON_WIDTH * DUNGEON_HEIGHT)), (uint8_t)(current_index % DUNGEON_WIDTH), (uint8_t)((current_index % (DUNGEON_WIDTH * DUNGEON_HEIGHT)) / DUNGEON_WIDTH) };
      3. FOR loopuint8_t option = AI_NORTH; option <= AI_DOWN; ++option

        Repeat body while the loop condition permits; then continue below.

          1. ExecuteDungeonPosition next;
          2. Decision!aiNeighbour(current, (ai_option_t)option, &next)
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          3. Executeconst uint16_t next_index = aiPositionIndex(next);
          4. Decisionai_route_first_option[next_index] != 0xFFU
            True
            1. Next loop iterationcontinue;
            False

            Continue after this decision.

          5. Executeai_route_first_option[next_index] = current_index == start_index ? option : ai_route_first_option[current_index];
          6. Decision!isDungeonExplored(next.floor, next.x, next.y)
            True
              1. Execute*first_option = (ai_option_t)ai_route_first_option[next_index];
              2. Exit this functionreturn true;
            False

            Continue after this decision.

          7. Executeai_route_queue[tail++] = next_index;
  8. Exit this functionreturn false;
Read original function source
static bool aiNearestUnexplored(ai_option_t *first_option)
{
    const DungeonPosition start = player_position;
    memset(ai_route_first_option, 0xFF, sizeof(ai_route_first_option));
    uint16_t head = 0U, tail = 0U;
    const uint16_t start_index = aiPositionIndex(start);
    ai_route_queue[tail++] = start_index;
    ai_route_first_option[start_index] = 0xFEU;
    while (head < tail) {
        const uint16_t current_index = ai_route_queue[head++];
        DungeonPosition current = { (uint8_t)(current_index / (DUNGEON_WIDTH * DUNGEON_HEIGHT)),
                                    (uint8_t)(current_index % DUNGEON_WIDTH),
                                    (uint8_t)((current_index % (DUNGEON_WIDTH * DUNGEON_HEIGHT)) / DUNGEON_WIDTH) };
        for (uint8_t option = AI_NORTH; option <= AI_DOWN; ++option) {
            DungeonPosition next;
            if (!aiNeighbour(current, (ai_option_t)option, &next)) continue;
            const uint16_t next_index = aiPositionIndex(next);
            if (ai_route_first_option[next_index] != 0xFFU) continue;
            ai_route_first_option[next_index] = current_index == start_index ? option : ai_route_first_option[current_index];
            if (!isDungeonExplored(next.floor, next.x, next.y)) { *first_option = (ai_option_t)ai_route_first_option[next_index]; return true; }
            ai_route_queue[tail++] = next_index;
        }
    }
    return false;
}
aiFindUncollectedKey() · source line 521
static bool aiFindUncollectedKey(DungeonPosition *position)

Calls into this documentation:

  1. ExecuteDungeonWorld *world = getDungeonWorld();
  2. FOR loopuint8_t floor = 0U; floor < DUNGEON_FLOOR_COUNT; ++floor

    Repeat body while the loop condition permits; then continue below.

    1. FOR loopuint8_t y = 0U; y < DUNGEON_HEIGHT; ++y

      Repeat body while the loop condition permits; then continue below.

      1. FOR loopuint8_t x = 0U; x < DUNGEON_WIDTH; ++x

        Repeat body while the loop condition permits; then continue below.

          1. Executeconst DungeonCell *cell = &world->floors[floor].cells[y][x];
          2. Decision(cell->eventFlags & DUNGEON_EVENT_KEY) != 0U && (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) == 0U
            True
              1. Execute*position = (DungeonPosition){floor, x, y};
              2. Exit this functionreturn true;
            False

            Continue after this decision.

  3. Exit this functionreturn false;
Read original function source
static bool aiFindUncollectedKey(DungeonPosition *position)
{
    DungeonWorld *world = getDungeonWorld();
    for (uint8_t floor = 0U; floor < DUNGEON_FLOOR_COUNT; ++floor)
        for (uint8_t y = 0U; y < DUNGEON_HEIGHT; ++y)
            for (uint8_t x = 0U; x < DUNGEON_WIDTH; ++x) {
                const DungeonCell *cell = &world->floors[floor].cells[y][x];
                if ((cell->eventFlags & DUNGEON_EVENT_KEY) != 0U &&
                    (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) == 0U) {
                    *position = (DungeonPosition){floor, x, y};
                    return true;
                }
            }
    return false;
}
aiChooseMove() · source line 537
static bool aiChooseMove(ai_option_t *option)

Calls into this documentation:

  1. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  2. ExecuteDungeonWorld *world = getDungeonWorld();
  3. ExecuteDungeonPosition target;
  4. Decisionplayer->keys == 0U && (player->runFlags & DUNGEON_RUN_KEY_REVEALED) != 0U && aiFindUncollectedKey(&target)
    True
    1. Exit this functionreturn aiFindPath(target, option);
    False

    Continue after this decision.

  5. Decisionplayer->keys > 0U && player_position.floor < DUNGEON_FLOOR_COUNT - 1U
    True
      1. FOR loopuint8_t index = 0U; index < DUNGEON_FLOOR_COUNT - 1U; ++index

        Repeat body while the loop condition permits; then continue below.

        1. Decisionworld->stairLinks[index].upper.floor == player_position.floor
          True
          1. Exit this functionreturn aiSamePosition(player_position, world->stairLinks[index].upper) ? ((*option = AI_DOWN), true) : aiFindPath(world->stairLinks[index].upper, option);
          False

          Continue after this decision.

    False

    Continue after this decision.

  6. Exit this functionreturn aiNearestUnexplored(option);
Read original function source
static bool aiChooseMove(ai_option_t *option)
{
    DungeonPlayerState *player = getDungeonPlayerState();
    DungeonWorld *world = getDungeonWorld();
    DungeonPosition target;
    if (player->keys == 0U && (player->runFlags & DUNGEON_RUN_KEY_REVEALED) != 0U &&
        aiFindUncollectedKey(&target)) return aiFindPath(target, option);
    if (player->keys > 0U && player_position.floor < DUNGEON_FLOOR_COUNT - 1U) {
        for (uint8_t index = 0U; index < DUNGEON_FLOOR_COUNT - 1U; ++index)
            if (world->stairLinks[index].upper.floor == player_position.floor)
                return aiSamePosition(player_position, world->stairLinks[index].upper) ? ((*option = AI_DOWN), true) : aiFindPath(world->stairLinks[index].upper, option);
    }
    return aiNearestUnexplored(option);
}
aiMoveOnce() · source line 552
static void aiMoveOnce(void)

Calls into this documentation:

  1. Executeai_option_t option;
  2. Decision!aiChooseMove(&option)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisionai_reverse_stair_blocked && ((option == AI_UP) == ai_blocked_stair_up)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisionoption == AI_UP || option == AI_DOWN
    True
      1. DecisionmoveStairs(option == AI_UP)
        True
          1. Executeai_reverse_stair_blocked = true;
          2. Executeai_blocked_stair_up = option == AI_DOWN;
          3. Executeai_shop_visited = false;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executeconst uint8_t exits[] = { DUNGEON_EXIT_NORTH, DUNGEON_EXIT_EAST, DUNGEON_EXIT_SOUTH, DUNGEON_EXIT_WEST };
  6. DecisionmovePlayer(exits[option])
    True
      1. Executeai_reverse_stair_blocked = false;
      2. Executeai_shop_visited = false;
    False

    Continue after this decision.

Read original function source
static void aiMoveOnce(void)
{
    ai_option_t option;
    if (!aiChooseMove(&option)) return;
    if (ai_reverse_stair_blocked && ((option == AI_UP) == ai_blocked_stair_up)) return;
    if (option == AI_UP || option == AI_DOWN) {
        if (moveStairs(option == AI_UP)) {
            ai_reverse_stair_blocked = true;
            ai_blocked_stair_up = option == AI_DOWN;
            ai_shop_visited = false;
        }
        return;
    }
    const uint8_t exits[] = { DUNGEON_EXIT_NORTH, DUNGEON_EXIT_EAST, DUNGEON_EXIT_SOUTH, DUNGEON_EXIT_WEST };
    if (movePlayer(exits[option])) {
        ai_reverse_stair_blocked = false;
        ai_shop_visited = false;
    }
}
aiChooseCombatAction() · source line 572
static void aiChooseCombatAction(void)

Calls into this documentation:

  1. Executeconst DungeonPlayerState *player = getDungeonPlayerState();
  2. Decisionplayer->health <= combat_state.health / 2U
    True
    1. Executeselected_combat_option = COMBAT_RUN;
    False
    1. Decisionplayer->healthPotions > 0U && player->health <= player->maxHealth / 2U
      True
      1. Executeselected_combat_option = COMBAT_POTION;
      False
      1. Executeselected_combat_option = COMBAT_ATTACK;
Read original function source
static void aiChooseCombatAction(void)
{
    const DungeonPlayerState *player = getDungeonPlayerState();
    if (player->health <= combat_state.health / 2U) selected_combat_option = COMBAT_RUN;
    else if (player->healthPotions > 0U && player->health <= player->maxHealth / 2U) selected_combat_option = COMBAT_POTION;
    else selected_combat_option = COMBAT_ATTACK;
}
startDungeon() · source line 583
void startDungeon(uint32_t now_ms)

Calls into this documentation:

  1. Executerunning = true;
  2. Executeplay_timer_running = false;
  3. Executelast_play_time_ms = now_ms;
  4. Executestate = DUNGEON_START_MENU;
  5. Executeselected_option = 0;
  6. Executeimage_visible = false;
  7. Executedecode_pending = false;
  8. Executewaiting_for_centre = true;
  9. Executeheld_direction = 0;
  10. Executerepeating = false;
  11. Executemap_visible = false;
  12. Executestatus_visible = false;
  13. Executemap_floor = 0;
  14. Executemap_page = 0;
  15. Executestatus_page = 0;
  16. Executereward_text[0] = '\0';
  17. Executecombat_text[0] = '\0';
  18. Executeshop_text[0] = '\0';
  19. Executecreation_trace[0] = '\0';
  20. Executetrap_damage = 0U;
  21. Executecombat_levels_gained = 0U;
  22. Executereward_is_key = false;
  23. Executecombat_victory = false;
  24. Executeai_mode = false;
  25. Executeai_start_at = 0U;
  26. Executeai_start_seconds = 2U;
  27. Executeai_last_move = 0U;
  28. Executeai_reverse_stair_blocked = false;
  29. Executeai_shop_visited = false;
  30. Executeai_last_shop_option = 4U;
  31. Executeai_speed_index = AI_SPEED_DEFAULT_INDEX;
  32. Executetrap_defeated = false;
  33. Executecombat_result_title = NULL;
  34. ExecuteinitDungeonConfig();
  35. ExecuteinitDungeonExplored();
  36. ExecuteinitDungeonPlayer();
  37. Executememset(&player_result, 0, sizeof(player_result));
  38. Executeplayer_result.error = DUNGEON_PLAYER_OK;
  39. Executememset(&explored_result, 0, sizeof(explored_result));
  40. Executeexplored_result.error = DUNGEON_EXPLORED_OK;
  41. Executememset(&history_result, 0, sizeof(history_result));
  42. Executehistory_result.error = DUNGEON_HISTORY_OK;
  43. Executecompletion_rank = 0U;
  44. Decision!loadDungeonWorld(&storage_result)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  45. Decision!prepareEntranceImage()
    True
      1. Executestorage_result.error = DUNGEON_STORAGE_READ_FAILED;
      2. Exit this functionreturn;
    False

    Continue after this decision.

  46. ExecuteprepareStartBackground();
Read original function source
void startDungeon(uint32_t now_ms)
{
    running = true;
    play_timer_running = false;
    last_play_time_ms = now_ms;
    state = DUNGEON_START_MENU;
    selected_option = 0;
    image_visible = false;
    decode_pending = false;
    waiting_for_centre = true;
    held_direction = 0;
    repeating = false;
    map_visible = false;
    status_visible = false;
    map_floor = 0;
    map_page = 0;
    status_page = 0;
    reward_text[0] = '\0';
    combat_text[0] = '\0';
    shop_text[0] = '\0';
    creation_trace[0] = '\0';
    trap_damage = 0U;
    combat_levels_gained = 0U;
    reward_is_key = false;
    combat_victory = false;
    ai_mode = false;
    ai_start_at = 0U;
    ai_start_seconds = 2U;
    ai_last_move = 0U;
    ai_reverse_stair_blocked = false;
    ai_shop_visited = false;
    ai_last_shop_option = 4U;
    ai_speed_index = AI_SPEED_DEFAULT_INDEX;
    trap_defeated = false;
    combat_result_title = NULL;
    initDungeonConfig();
    initDungeonExplored();
    initDungeonPlayer();
    memset(&player_result, 0, sizeof(player_result));
    player_result.error = DUNGEON_PLAYER_OK;
    memset(&explored_result, 0, sizeof(explored_result));
    explored_result.error = DUNGEON_EXPLORED_OK;
    memset(&history_result, 0, sizeof(history_result));
    history_result.error = DUNGEON_HISTORY_OK;
    completion_rank = 0U;
    if (!loadDungeonWorld(&storage_result)) return;
    if (!prepareEntranceImage()) {
        storage_result.error = DUNGEON_STORAGE_READ_FAILED;
        return;
    }
    prepareStartBackground();
}
stopDungeon() · source line 636
void stopDungeon(uint32_t now_ms)

Calls into this documentation:

  1. ExecutecommitPlayTime(now_ms);
  2. Decisionplay_timer_running
    True
    1. Execute(void)saveDungeonPlayer(&player_result);
    False

    Continue after this decision.

  3. Executeplay_timer_running = false;
  4. Executerunning = false;
  5. Executedecode_pending = false;
  6. Executeimage_visible = false;
Read original function source
void stopDungeon(uint32_t now_ms)
{
    commitPlayTime(now_ms);
    if (play_timer_running) (void)saveDungeonPlayer(&player_result);
    play_timer_running = false;
    running = false;
    decode_pending = false;
    image_visible = false;
}
combatOptionCount() · source line 646
static unsigned combatOptionCount(void)

Calls into this documentation:

  1. Exit this functionreturn getDungeonPlayerState()->healthPotions > 0U ? 3U : 2U;
Read original function source
static unsigned combatOptionCount(void)
{
    return getDungeonPlayerState()->healthPotions > 0U ? 3U : 2U;
}
combatEnemyName() · source line 651
static const char *combatEnemyName(uint8_t enemy_type)
  1. Select a caseenemy_type

    Enter matching case; follow source fall-through until a break, return or end.

      1. Case / labelcase DUNGEON_ENEMY_LEVEL_1:
      2. Exit this functionreturn "LEVEL 1";
      3. Case / labelcase DUNGEON_ENEMY_LEVEL_2:
      4. Exit this functionreturn "LEVEL 2";
      5. Case / labelcase DUNGEON_ENEMY_LEVEL_3:
      6. Exit this functionreturn "LEVEL 3";
      7. Case / labelcase DUNGEON_ENEMY_LEVEL_4:
      8. Exit this functionreturn "LEVEL 4";
      9. Case / labeldefault:
      10. Exit this functionreturn "DRAGON";
Read original function source
static const char *combatEnemyName(uint8_t enemy_type)
{
    switch (enemy_type) {
        case DUNGEON_ENEMY_LEVEL_1: return "LEVEL 1";
        case DUNGEON_ENEMY_LEVEL_2: return "LEVEL 2";
        case DUNGEON_ENEMY_LEVEL_3: return "LEVEL 3";
        case DUNGEON_ENEMY_LEVEL_4: return "LEVEL 4";
        default: return "DRAGON";
    }
}
returnToRoom() · source line 662
static void returnToRoom(void)

Calls into this documentation:

  1. Decision!prepareRoomImage(&player_position)
    True
      1. Executeplayer_result.error = DUNGEON_PLAYER_INVALID_POSITION;
      2. Executestate = DUNGEON_NOTICE;
      3. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executestate = DUNGEON_ROOM;
  3. Executeimage_visible = false;
  4. Executedecode_pending = true;
Read original function source
static void returnToRoom(void)
{
    if (!prepareRoomImage(&player_position)) {
        player_result.error = DUNGEON_PLAYER_INVALID_POSITION;
        state = DUNGEON_NOTICE;
        return;
    }
    state = DUNGEON_ROOM;
    image_visible = false;
    decode_pending = true;
}
beginDungeonCreation() · source line 674
static void beginDungeonCreation(bool delete_old_files, uint32_t now_ms)
  1. Executememset(&creation_generation_stats, 0, sizeof(creation_generation_stats));
  2. Executecreation_step = delete_old_files ? CREATE_DELETE : CREATE_GENERATE;
  3. Executecreation_step_at = now_ms;
  4. Executestate = DUNGEON_CREATING;
  5. Executeimage_visible = false;
  6. Executedecode_pending = false;
Read original function source
static void beginDungeonCreation(bool delete_old_files, uint32_t now_ms)
{
    memset(&creation_generation_stats, 0, sizeof(creation_generation_stats));
    creation_step = delete_old_files ? CREATE_DELETE : CREATE_GENERATE;
    creation_step_at = now_ms;
    state = DUNGEON_CREATING;
    image_visible = false;
    decode_pending = false;
}
advanceDungeonCreation() · source line 684
static void advanceDungeonCreation(uint32_t now_ms)

Calls into this documentation:

  1. Decisionnow_ms - creation_step_at < DUNGEON_CREATION_STAGE_PRESENT_MS
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisioncreation_step == CREATE_DELETE
    True
      1. Decision!deleteDungeonDataFiles(&delete_result)
        True
          1. Executestate = DUNGEON_NOTICE;
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. ExecuteprepareNewDungeonWorldStorage();
      3. Executecreation_step = CREATE_GENERATE;
      4. Executecreation_step_at = now_ms;
      5. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisioncreation_step == CREATE_GENERATE
    True
      1. Decision!generateDungeonWorld(&creation_generation_stats)
        True
          1. Executestate = DUNGEON_NOTICE;
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executecreation_step = CREATE_WORLD;
      3. Executecreation_step_at = now_ms;
      4. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisioncreation_step == CREATE_WORLD
    True
      1. Decision!saveDungeonWorld(&storage_result)
        True
          1. Executestate = DUNGEON_NOTICE;
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executecreation_step = CREATE_PLAYER;
      3. Executecreation_step_at = now_ms;
      4. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisioncreation_step == CREATE_PLAYER
    True
      1. Decision!resetDungeonPlayer(&player_result)
        True
          1. Executestate = DUNGEON_NOTICE;
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executecreation_step = CREATE_EXPLORED;
      3. Executecreation_step_at = now_ms;
      4. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisioncreation_step == CREATE_EXPLORED
    True
      1. Decision!resetDungeonExplored(&getDungeonPlayerState()->position, &explored_result)
        True
          1. Executestate = DUNGEON_NOTICE;
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executeplayer_position = getDungeonPlayerState()->position;
      3. Executecreation_step = CREATE_READY;
      4. Executecreation_ready_at = now_ms;
      5. Executecreation_step_at = now_ms;
      6. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionnow_ms - creation_ready_at < DUNGEON_CREATION_READY_MS
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  8. Decision!prepareRoomImage(&player_position)
    True
      1. Executeplayer_result.error = DUNGEON_PLAYER_INVALID_POSITION;
      2. Executestate = DUNGEON_NOTICE;
      3. Exit this functionreturn;
    False

    Continue after this decision.

  9. Executestate = DUNGEON_ROOM;
  10. ExecutestartPlayTimer(now_ms);
  11. Executeimage_visible = false;
  12. Executedecode_pending = true;
Read original function source
static void advanceDungeonCreation(uint32_t now_ms)
{
    if (now_ms - creation_step_at < DUNGEON_CREATION_STAGE_PRESENT_MS) return;
    if (creation_step == CREATE_DELETE) {
        if (!deleteDungeonDataFiles(&delete_result)) { state = DUNGEON_NOTICE; return; }
        prepareNewDungeonWorldStorage();
        creation_step = CREATE_GENERATE;
        creation_step_at = now_ms;
        return;
    }
    if (creation_step == CREATE_GENERATE) {
        if (!generateDungeonWorld(&creation_generation_stats)) { state = DUNGEON_NOTICE; return; }
        creation_step = CREATE_WORLD;
        creation_step_at = now_ms;
        return;
    }
    if (creation_step == CREATE_WORLD) {
        if (!saveDungeonWorld(&storage_result)) { state = DUNGEON_NOTICE; return; }
        creation_step = CREATE_PLAYER;
        creation_step_at = now_ms;
        return;
    }
    if (creation_step == CREATE_PLAYER) {
        if (!resetDungeonPlayer(&player_result)) { state = DUNGEON_NOTICE; return; }
        creation_step = CREATE_EXPLORED;
        creation_step_at = now_ms;
        return;
    }
    if (creation_step == CREATE_EXPLORED) {
        if (!resetDungeonExplored(&getDungeonPlayerState()->position, &explored_result)) { state = DUNGEON_NOTICE; return; }
        player_position = getDungeonPlayerState()->position;
        creation_step = CREATE_READY;
        creation_ready_at = now_ms;
        creation_step_at = now_ms;
        return;
    }
    if (now_ms - creation_ready_at < DUNGEON_CREATION_READY_MS) return;
    if (!prepareRoomImage(&player_position)) { player_result.error = DUNGEON_PLAYER_INVALID_POSITION; state = DUNGEON_NOTICE; return; }
    state = DUNGEON_ROOM;
    startPlayTimer(now_ms);
    image_visible = false;
    decode_pending = true;
}
completeCombatVictory() · source line 728
static void completeCombatVictory(void)

Calls into this documentation:

  1. ExecuteDungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
  2. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  3. Executeconst DungeonPlayerState previous_player = *player;
  4. Executeconst uint16_t previous_flags = cell->stateFlags;
  5. Executeconst DungeonPosition *exit = &getDungeonWorld()->exit;
  6. Executeconst bool final_dragon = combat_state.enemyType == DUNGEON_ENEMY_DRAGON && player_position.floor == exit->floor && player_position.x == exit->x && player_position.y == exit->y;
  7. Executecombat_levels_gained = addDungeonPlayerExperience(combat_state.xpReward);
  8. Executecell->stateFlags |= DUNGEON_STATE_COMPLETED;
  9. Decisionfinal_dragon
    True
      1. Executecell->stateFlags |= DUNGEON_STATE_MESSAGE_READ | DUNGEON_STATE_UNLOCKED;
      2. Executeplayer->runFlags |= DUNGEON_RUN_COMPLETED;
    False

    Continue after this decision.

  10. Decision!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)
    True
      1. Execute*player = previous_player;
      2. Executecell->stateFlags = previous_flags;
      3. ExecutesaveDungeonWorld(&storage_result);
      4. Executestate = DUNGEON_NOTICE;
      5. Exit this functionreturn;
    False

    Continue after this decision.

  11. Decisionfinal_dragon
    True
      1. ExecuterecordDungeonCompletion(player, &history_result, &completion_rank);
      2. Executestate = DUNGEON_COMPLETE;
      3. Exit this functionreturn;
    False

    Continue after this decision.

  12. Decisioncombat_levels_gained > 0U
    True
    1. Executesnprintf(combat_text, sizeof(combat_text), "+%lu XP LEVEL +%u", (unsigned long)combat_state.xpReward, (unsigned)combat_levels_gained);
    False
    1. Executesnprintf(combat_text, sizeof(combat_text), "+%lu XP", (unsigned long)combat_state.xpReward);
  13. Executecombat_victory = true;
  14. Executecombat_result_title = "VICTORY";
  15. Executecombat_result_color = COLOR_GREEN;
  16. Executestate = DUNGEON_COMBAT_RESULT;
Read original function source
static void completeCombatVictory(void)
{
    DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    const uint16_t previous_flags = cell->stateFlags;
    const DungeonPosition *exit = &getDungeonWorld()->exit;
    const bool final_dragon = combat_state.enemyType == DUNGEON_ENEMY_DRAGON && player_position.floor == exit->floor && player_position.x == exit->x && player_position.y == exit->y;
    combat_levels_gained = addDungeonPlayerExperience(combat_state.xpReward);
    cell->stateFlags |= DUNGEON_STATE_COMPLETED;
    if (final_dragon) {
        cell->stateFlags |= DUNGEON_STATE_MESSAGE_READ | DUNGEON_STATE_UNLOCKED;
        player->runFlags |= DUNGEON_RUN_COMPLETED;
    }
    if (!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        cell->stateFlags = previous_flags;
        saveDungeonWorld(&storage_result);
        state = DUNGEON_NOTICE;
        return;
    }
    if (final_dragon) {
        recordDungeonCompletion(player, &history_result, &completion_rank);
        state = DUNGEON_COMPLETE;
        return;
    }
    if (combat_levels_gained > 0U) snprintf(combat_text, sizeof(combat_text), "+%lu XP  LEVEL +%u", (unsigned long)combat_state.xpReward, (unsigned)combat_levels_gained);
    else snprintf(combat_text, sizeof(combat_text), "+%lu XP", (unsigned long)combat_state.xpReward);
    combat_victory = true;
    combat_result_title = "VICTORY";
    combat_result_color = COLOR_GREEN;
    state = DUNGEON_COMBAT_RESULT;
}
respawnAfterCombatDefeat() · source line 762
static void respawnAfterCombatDefeat(void)

Calls into this documentation:

  1. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  2. Executeconst DungeonPlayerState previous_player = *player;
  3. ExecuteDungeonPosition respawn = getDungeonWorld()->entrance;
  4. Decisionplayer_position.floor > 0U
    True
    1. Executerespawn = getDungeonWorld()->stairLinks[player_position.floor - 1U].lower;
    False

    Continue after this decision.

  5. Executeplayer->position = respawn;
  6. Executeplayer->health = (uint16_t)(player->maxHealth / 2U + player->maxHealth % 2U);
  7. Decision!saveDungeonPlayer(&player_result)
    True
      1. Execute*player = previous_player;
      2. Executestate = DUNGEON_NOTICE;
      3. Exit this functionreturn;
    False

    Continue after this decision.

  8. Executeplayer_position = respawn;
  9. ExecuterecordExplored(&player_position);
  10. Executesnprintf(combat_text, sizeof(combat_text), "RETURNED WITH HALF HEALTH");
  11. Executecombat_victory = false;
  12. Executecombat_result_title = "DEFEATED";
  13. Executecombat_result_color = COLOR_RED;
  14. Executestate = DUNGEON_COMBAT_RESULT;
Read original function source
static void respawnAfterCombatDefeat(void)
{
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    DungeonPosition respawn = getDungeonWorld()->entrance;
    if (player_position.floor > 0U) respawn = getDungeonWorld()->stairLinks[player_position.floor - 1U].lower;
    player->position = respawn;
    player->health = (uint16_t)(player->maxHealth / 2U + player->maxHealth % 2U);
    if (!saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        state = DUNGEON_NOTICE;
        return;
    }
    player_position = respawn;
    recordExplored(&player_position);
    snprintf(combat_text, sizeof(combat_text), "RETURNED WITH HALF HEALTH");
    combat_victory = false;
    combat_result_title = "DEFEATED";
    combat_result_color = COLOR_RED;
    state = DUNGEON_COMBAT_RESULT;
}
resolveCombatAction() · source line 784
static void resolveCombatAction(void)

Calls into this documentation:

  1. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  2. ExecuteDungeonCombatResult result;
  3. Decisionselected_combat_option == COMBAT_RUN
    True
      1. Executeresult = dungeonCombatAttemptRun(&combat_state, player);
      2. Decisionresult == DUNGEON_COMBAT_ESCAPED
        True
          1. Executeplayer->position = encounter_source;
          2. Decision!saveDungeonPlayer(&player_result)
            True
              1. Executestate = DUNGEON_NOTICE;
              2. Exit this functionreturn;
            False

            Continue after this decision.

          3. Executeplayer_position = encounter_source;
          4. Executesnprintf(combat_text, sizeof(combat_text), "ESCAPED");
          5. Executecombat_victory = false;
          6. Executecombat_result_title = "ESCAPED";
          7. Executecombat_result_color = COLOR_CYAN;
          8. Executestate = DUNGEON_COMBAT_RESULT;
          9. Exit this functionreturn;
        False

        Continue after this decision.

      3. Executesnprintf(combat_text, sizeof(combat_text), "RUN FAILED ENEMY %lu", (unsigned long)combat_state.lastEnemyDamage);
    False
    1. Decisionselected_combat_option == COMBAT_POTION
      True
        1. Executeresult = dungeonCombatUsePotion(&combat_state, player);
        2. Executesnprintf(combat_text, sizeof(combat_text), "POTION ENEMY %lu", (unsigned long)combat_state.lastEnemyDamage);
      False
        1. Executeresult = dungeonCombatAttack(&combat_state, player);
        2. Executesnprintf(combat_text, sizeof(combat_text), "YOU %lu ENEMY %lu", (unsigned long)combat_state.lastPlayerDamage, (unsigned long)combat_state.lastEnemyDamage);
  4. Decisionresult == DUNGEON_COMBAT_ENEMY_DEFEATED
    True
      1. ExecutecompleteCombatVictory();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisionresult == DUNGEON_COMBAT_PLAYER_DEFEATED
    True
      1. ExecuterespawnAfterCombatDefeat();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisionselected_combat_option >= combatOptionCount()
    True
    1. Executeselected_combat_option = COMBAT_ATTACK;
    False

    Continue after this decision.

  7. Decision!saveDungeonPlayer(&player_result)
    True
    1. Executestate = DUNGEON_NOTICE;
    False

    Continue after this decision.

Read original function source
static void resolveCombatAction(void)
{
    DungeonPlayerState *player = getDungeonPlayerState();
    DungeonCombatResult result;
    if (selected_combat_option == COMBAT_RUN) {
        result = dungeonCombatAttemptRun(&combat_state, player);
        if (result == DUNGEON_COMBAT_ESCAPED) {
            player->position = encounter_source;
            if (!saveDungeonPlayer(&player_result)) { state = DUNGEON_NOTICE; return; }
            player_position = encounter_source;
            snprintf(combat_text, sizeof(combat_text), "ESCAPED");
            combat_victory = false;
            combat_result_title = "ESCAPED";
            combat_result_color = COLOR_CYAN;
            state = DUNGEON_COMBAT_RESULT;
            return;
        }
        snprintf(combat_text, sizeof(combat_text), "RUN FAILED  ENEMY %lu", (unsigned long)combat_state.lastEnemyDamage);
    } else if (selected_combat_option == COMBAT_POTION) {
        result = dungeonCombatUsePotion(&combat_state, player);
        snprintf(combat_text, sizeof(combat_text), "POTION  ENEMY %lu", (unsigned long)combat_state.lastEnemyDamage);
    } else {
        result = dungeonCombatAttack(&combat_state, player);
        snprintf(combat_text, sizeof(combat_text), "YOU %lu  ENEMY %lu", (unsigned long)combat_state.lastPlayerDamage, (unsigned long)combat_state.lastEnemyDamage);
    }
    if (result == DUNGEON_COMBAT_ENEMY_DEFEATED) { completeCombatVictory(); return; }
    if (result == DUNGEON_COMBAT_PLAYER_DEFEATED) { respawnAfterCombatDefeat(); return; }
    if (selected_combat_option >= combatOptionCount()) selected_combat_option = COMBAT_ATTACK;
    if (!saveDungeonPlayer(&player_result)) state = DUNGEON_NOTICE;
}
shopUpgradeCost() · source line 815
static uint32_t shopUpgradeCost(uint32_t base_cost)

Calls into this documentation:

  1. Executeconst DungeonConfig *config = getDungeonConfig();
  2. Executeuint32_t cost = base_cost;
  3. FOR loopuint16_t purchase = 0U; purchase < getDungeonPlayerState()->shopPurchaseCount; ++purchase

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst uint64_t increased = ((uint64_t)cost * (100U + config->shopCostIncreasePercent) + 99U) / 100U;
      2. Decisionincreased >= UINT32_MAX
        True
        1. Exit this functionreturn UINT32_MAX;
        False

        Continue after this decision.

      3. Executecost = (uint32_t)increased;
  4. Exit this functionreturn cost;
Read original function source
static uint32_t shopUpgradeCost(uint32_t base_cost)
{
    const DungeonConfig *config = getDungeonConfig();
    uint32_t cost = base_cost;
    for (uint16_t purchase = 0U; purchase < getDungeonPlayerState()->shopPurchaseCount; ++purchase) {
        const uint64_t increased = ((uint64_t)cost * (100U + config->shopCostIncreasePercent) + 99U) / 100U;
        if (increased >= UINT32_MAX) return UINT32_MAX;
        cost = (uint32_t)increased;
    }
    return cost;
}
shopChoiceCost() · source line 827
static uint32_t shopChoiceCost(unsigned choice)

Calls into this documentation:

  1. Executeconst DungeonConfig *config = getDungeonConfig();
  2. Decisionchoice == 0U
    True
    1. Exit this functionreturn config->shopRestoreCost;
    False

    Continue after this decision.

  3. Decisionchoice == 1U
    True
    1. Exit this functionreturn shopUpgradeCost(config->shopHealthBaseCost);
    False

    Continue after this decision.

  4. Decisionchoice == 2U
    True
    1. Exit this functionreturn shopUpgradeCost(config->shopAttackBaseCost);
    False

    Continue after this decision.

  5. Exit this functionreturn choice == 3U ? shopUpgradeCost(config->shopDefenceBaseCost) : 0U;
Read original function source
static uint32_t shopChoiceCost(unsigned choice)
{
    const DungeonConfig *config = getDungeonConfig();
    if (choice == 0U) return config->shopRestoreCost;
    if (choice == 1U) return shopUpgradeCost(config->shopHealthBaseCost);
    if (choice == 2U) return shopUpgradeCost(config->shopAttackBaseCost);
    return choice == 3U ? shopUpgradeCost(config->shopDefenceBaseCost) : 0U;
}
applyShopChoice() · source line 836
static void applyShopChoice(void)

Calls into this documentation:

  1. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
  2. Executeconst DungeonPlayerState previous_player = *player;
  3. Executeconst DungeonConfig *config = getDungeonConfig();
  4. Decisionselected_shop_option == 4U
    True
      1. ExecutereturnToRoom();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Executeconst uint32_t cost = shopChoiceCost(selected_shop_option);
  6. Decisionplayer->gold < cost
    True
      1. Executesnprintf(shop_text, sizeof(shop_text), "NOT ENOUGH GOLD");
      2. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionselected_shop_option == 0U
    True
      1. Decisionplayer->health >= player->maxHealth
        True
          1. Executesnprintf(shop_text, sizeof(shop_text), "HEALTH ALREADY FULL");
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executeplayer->health = player->maxHealth;
      3. Executesnprintf(shop_text, sizeof(shop_text), "HEALTH RESTORED");
    False
    1. Decisionselected_shop_option == 1U
      True
        1. Decisionconfig->shopHealthGain == 0U || config->shopHealthGain > UINT16_MAX - player->maxHealth
          True
            1. Executesnprintf(shop_text, sizeof(shop_text), "MAX HP LIMIT");
            2. Exit this functionreturn;
          False

          Continue after this decision.

        2. Executeplayer->maxHealth += config->shopHealthGain;
        3. Executeplayer->health = player->health <= UINT16_MAX - config->shopHealthGain ? player->health + config->shopHealthGain : UINT16_MAX;
        4. Executesnprintf(shop_text, sizeof(shop_text), "MAX HP INCREASED");
      False
      1. Decisionselected_shop_option == 2U
        True
          1. Decisionconfig->shopAttackGain == 0U || config->shopAttackGain > UINT16_MAX - player->attack
            True
              1. Executesnprintf(shop_text, sizeof(shop_text), "ATTACK LIMIT");
              2. Exit this functionreturn;
            False

            Continue after this decision.

          2. Executeplayer->attack += config->shopAttackGain;
          3. Executesnprintf(shop_text, sizeof(shop_text), "ATTACK INCREASED");
        False
          1. Decisionconfig->shopDefenceGain == 0U || config->shopDefenceGain > UINT16_MAX - player->defence
            True
              1. Executesnprintf(shop_text, sizeof(shop_text), "DEFENCE LIMIT");
              2. Exit this functionreturn;
            False

            Continue after this decision.

          2. Executeplayer->defence += config->shopDefenceGain;
          3. Executesnprintf(shop_text, sizeof(shop_text), "DEFENCE INCREASED");
  8. Executeplayer->gold -= cost;
  9. Decisionplayer->shopPurchaseCount < UINT16_MAX
    True
    1. Execute++player->shopPurchaseCount;
    False

    Continue after this decision.

  10. Decision!saveDungeonPlayer(&player_result)
    True
      1. Execute*player = previous_player;
      2. Executestate = DUNGEON_NOTICE;
    False

    Continue after this decision.

Read original function source
static void applyShopChoice(void)
{
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    const DungeonConfig *config = getDungeonConfig();
    if (selected_shop_option == 4U) { returnToRoom(); return; }
    const uint32_t cost = shopChoiceCost(selected_shop_option);
    if (player->gold < cost) { snprintf(shop_text, sizeof(shop_text), "NOT ENOUGH GOLD"); return; }
    if (selected_shop_option == 0U) {
        if (player->health >= player->maxHealth) { snprintf(shop_text, sizeof(shop_text), "HEALTH ALREADY FULL"); return; }
        player->health = player->maxHealth;
        snprintf(shop_text, sizeof(shop_text), "HEALTH RESTORED");
    } else if (selected_shop_option == 1U) {
        if (config->shopHealthGain == 0U || config->shopHealthGain > UINT16_MAX - player->maxHealth) { snprintf(shop_text, sizeof(shop_text), "MAX HP LIMIT"); return; }
        player->maxHealth += config->shopHealthGain;
        player->health = player->health <= UINT16_MAX - config->shopHealthGain ? player->health + config->shopHealthGain : UINT16_MAX;
        snprintf(shop_text, sizeof(shop_text), "MAX HP INCREASED");
    } else if (selected_shop_option == 2U) {
        if (config->shopAttackGain == 0U || config->shopAttackGain > UINT16_MAX - player->attack) { snprintf(shop_text, sizeof(shop_text), "ATTACK LIMIT"); return; }
        player->attack += config->shopAttackGain;
        snprintf(shop_text, sizeof(shop_text), "ATTACK INCREASED");
    } else {
        if (config->shopDefenceGain == 0U || config->shopDefenceGain > UINT16_MAX - player->defence) { snprintf(shop_text, sizeof(shop_text), "DEFENCE LIMIT"); return; }
        player->defence += config->shopDefenceGain;
        snprintf(shop_text, sizeof(shop_text), "DEFENCE INCREASED");
    }
    player->gold -= cost;
    if (player->shopPurchaseCount < UINT16_MAX) ++player->shopPurchaseCount;
    if (!saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        state = DUNGEON_NOTICE;
    }
}
aiChooseShopOption() · source line 870
static unsigned aiChooseShopOption(void)

Calls into this documentation:

  1. Executeconst DungeonPlayerState *player = getDungeonPlayerState();
  2. Executeconst DungeonConfig *config = getDungeonConfig();
  3. Decision(uint32_t)player->health * 2U < player->maxHealth && player->health < player->maxHealth && player->gold >= shopChoiceCost(0U)
    True
    1. Exit this functionreturn 0U;
    False

    Continue after this decision.

  4. Executeconst unsigned options[] = {1U, 2U, 3U};
  5. Executeconst uint8_t weights[] = {2U, 1U, 4U};
  6. Executeuint8_t total = 0U;
  7. FOR loopunsigned index = 0U; index < 3U; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst unsigned option = options[index];
      2. Executeconst bool valid = option == 1U ? config->shopHealthGain > 0U && config->shopHealthGain <= UINT16_MAX - player->maxHealth : option == 2U ? config->shopAttackGain > 0U && config->shopAttackGain <= UINT16_MAX - player->attack : config->shopDefenceGain > 0U && config->shopDefenceGain <= UINT16_MAX - player->defence;
      3. Decisionvalid && player->gold >= shopChoiceCost(option) && option != ai_last_shop_option
        True
        1. Executetotal += weights[index];
        False

        Continue after this decision.

  8. Decisiontotal == 0U
    True
    1. Exit this functionreturn 4U;
    False

    Continue after this decision.

  9. Executeuint8_t ticket = (uint8_t)(esp_random() % total);
  10. FOR loopunsigned index = 0U; index < 3U; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst unsigned option = options[index];
      2. Executeconst bool valid = option == 1U ? config->shopHealthGain > 0U && config->shopHealthGain <= UINT16_MAX - player->maxHealth : option == 2U ? config->shopAttackGain > 0U && config->shopAttackGain <= UINT16_MAX - player->attack : config->shopDefenceGain > 0U && config->shopDefenceGain <= UINT16_MAX - player->defence;
      3. Decision!valid || player->gold < shopChoiceCost(option) || option == ai_last_shop_option
        True
        1. Next loop iterationcontinue;
        False

        Continue after this decision.

      4. Decisionticket < weights[index]
        True
        1. Exit this functionreturn option;
        False

        Continue after this decision.

      5. Executeticket = (uint8_t)(ticket - weights[index]);
  11. Exit this functionreturn 4U;
Read original function source
static unsigned aiChooseShopOption(void)
{
    const DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonConfig *config = getDungeonConfig();
    if ((uint32_t)player->health * 2U < player->maxHealth &&
        player->health < player->maxHealth && player->gold >= shopChoiceCost(0U)) return 0U;
    const unsigned options[] = {1U, 2U, 3U};
    const uint8_t weights[] = {2U, 1U, 4U};
    uint8_t total = 0U;
    for (unsigned index = 0U; index < 3U; ++index) {
        const unsigned option = options[index];
        const bool valid = option == 1U ? config->shopHealthGain > 0U && config->shopHealthGain <= UINT16_MAX - player->maxHealth :
                           option == 2U ? config->shopAttackGain > 0U && config->shopAttackGain <= UINT16_MAX - player->attack :
                                          config->shopDefenceGain > 0U && config->shopDefenceGain <= UINT16_MAX - player->defence;
        if (valid && player->gold >= shopChoiceCost(option) && option != ai_last_shop_option) total += weights[index];
    }
    if (total == 0U) return 4U;
    uint8_t ticket = (uint8_t)(esp_random() % total);
    for (unsigned index = 0U; index < 3U; ++index) {
        const unsigned option = options[index];
        const bool valid = option == 1U ? config->shopHealthGain > 0U && config->shopHealthGain <= UINT16_MAX - player->maxHealth :
                           option == 2U ? config->shopAttackGain > 0U && config->shopAttackGain <= UINT16_MAX - player->attack :
                                          config->shopDefenceGain > 0U && config->shopDefenceGain <= UINT16_MAX - player->defence;
        if (!valid || player->gold < shopChoiceCost(option) || option == ai_last_shop_option) continue;
        if (ticket < weights[index]) return option;
        ticket = (uint8_t)(ticket - weights[index]);
    }
    return 4U;
}
dungeonAction() · source line 903
void dungeonAction(bool action, bool map_action, bool status_action, bool context_action, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionstate == DUNGEON_COMPLETE && action
    True
      1. Executemenu_back(now_ms);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisionstate == DUNGEON_HISTORY && action
    True
      1. Executestate = DUNGEON_START_MENU;
      2. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisionstate == DUNGEON_SHOP
    True
      1. Decisionstatus_action
        True
          1. ExecutereturnToRoom();
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Decisionaction
        True
        1. ExecuteapplyShopChoice();
        False

        Continue after this decision.

      3. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisionstate == DUNGEON_LEVEL_BONUS
    True
      1. Decisionstatus_action
        True
          1. ExecutereturnToRoom();
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Decisionaction
        True
          1. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
          2. Executeconst DungeonPlayerState previous_player = *player;
          3. DecisionapplyDungeonPlayerStatChoice((uint8_t)selected_stat_option) && !saveDungeonPlayer(&player_result)
            True
              1. Execute*player = previous_player;
              2. Executestate = DUNGEON_NOTICE;
            False
            1. Decisionplayer->unspentStatPoints == 0U
              True
              1. ExecutereturnToRoom();
              False

              Continue after this decision.

        False

        Continue after this decision.

      3. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisionstate == DUNGEON_LOCKED_DOOR && action
    True
      1. ExecuteDungeonPlayerState *player = getDungeonPlayerState();
      2. Executeplayer->position = encounter_source;
      3. Decision!saveDungeonPlayer(&player_result)
        True
          1. Executestate = DUNGEON_NOTICE;
          2. Exit this functionreturn;
        False

        Continue after this decision.

      4. Executeplayer_position = encounter_source;
      5. ExecutereturnToRoom();
      6. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionstate == DUNGEON_TRAP_RESULT && action
    True
      1. Decision!trap_defeated && startRoomEvent(&player_position)
        True
        1. Exit this functionreturn;
        False

        Continue after this decision.

      2. ExecutereturnToRoom();
      3. Exit this functionreturn;
    False

    Continue after this decision.

  8. Decisionstate == DUNGEON_COMBAT_RESULT && action
    True
      1. Decisioncombat_victory && startRoomEvent(&player_position)
        True
        1. Exit this functionreturn;
        False

        Continue after this decision.

      2. ExecutereturnToRoom();
      3. Exit this functionreturn;
    False

    Continue after this decision.

  9. Decisionstate == DUNGEON_COMBAT && action
    True
      1. ExecuteresolveCombatAction();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  10. Decisionstate == DUNGEON_REWARD && action
    True
      1. ExecutereturnToRoom();
      2. Exit this functionreturn;
    False

    Continue after this decision.

  11. Decisionstate == DUNGEON_ROOM && map_action
    True
      1. Decisionmap_visible
        True
          1. Executemap_visible = false;
          2. Executewaiting_for_centre = true;
          3. ExecutereturnToRoom();
          4. Exit this functionreturn;
        False

        Continue after this decision.

    False

    Continue after this decision.

  12. Decisionstate == DUNGEON_ROOM && status_visible && context_action && status_page == 0U && getDungeonPlayerState()->unspentStatPoints > 0U
    True
      1. Executestatus_visible = false;
      2. Executeselected_stat_option = 0U;
      3. Executestate = DUNGEON_LEVEL_BONUS;
      4. Exit this functionreturn;
    False

    Continue after this decision.

  13. Decisionstate == DUNGEON_ROOM && !map_visible && !status_visible && context_action && isShopRoom(&getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x])
    True
      1. Executeselected_shop_option = 0U;
      2. Executeshop_text[0] = '\0';
      3. Executestate = DUNGEON_SHOP;
      4. Executeimage_visible = false;
      5. Executedecode_pending = false;
      6. Exit this functionreturn;
    False

    Continue after this decision.

  14. Decisionstate == DUNGEON_ROOM && status_action
    True
      1. Executemap_visible = false;
      2. Executestatus_visible = !status_visible;
      3. Decision!status_visible
        True
          1. Executeimage_visible = false;
          2. Executedecode_pending = true;
        False

        Continue after this decision.

      4. Executewaiting_for_centre = true;
      5. Exit this functionreturn;
    False

    Continue after this decision.

  15. Decisionstate == DUNGEON_ROOM && status_visible
    True
      1. Decisionaction
        True
        1. Executestatus_page = (status_page + 1U) % 3U;
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  16. Decisionstate == DUNGEON_ROOM && map_visible
    True
      1. Decisionaction
        True
          1. Decisionmap_page == 0U
            True
            1. Executemap_page = 1U;
            False
            1. Decisionmap_page == 1U
              True
              1. Executemap_page = 3U;
              False
              1. Decisionmap_page == 3U
                True
                1. Executemap_page = 2U;
                False
                  1. Executemap_page = 0U;
                  2. Executemap_floor = (map_floor + 1U) % DUNGEON_FLOOR_COUNT;
        False

        Continue after this decision.

      2. Executewaiting_for_centre = true;
      3. Exit this functionreturn;
    False

    Continue after this decision.

  17. Decisionstate == DUNGEON_ROOM && map_action
    True
      1. Executemap_floor = player_position.floor;
      2. Executemap_page = (player_position.y / 20U) * 2U + (player_position.x / 20U);
      3. Executemap_visible = true;
      4. Executewaiting_for_centre = true;
      5. Exit this functionreturn;
    False

    Continue after this decision.

  18. Decision!action
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  19. Decisionstate == DUNGEON_START_MENU && storage_result.error != DUNGEON_STORAGE_OK
    True
      1. ExecutetraceDungeonCreation("A RECEIVED", 0, 0);
      2. ExecutebeginDungeonCreation(true, now_ms);
      3. Exit this functionreturn;
    False

    Continue after this decision.

  20. Decisionstate == DUNGEON_START_MENU && storage_result.error == DUNGEON_STORAGE_OK
    True
      1. Decisionselected_option == 0U
        True
          1. Decision!prepareDungeonPlayer(&player_result)
            True
              1. Executestate = DUNGEON_NOTICE;
              2. Exit this functionreturn;
            False

            Continue after this decision.

          2. Executestate = DUNGEON_ROOM;
          3. Executeplayer_position = getDungeonPlayerState()->position;
          4. Decision!prepareRoomImage(&player_position)
            True
              1. Executestate = DUNGEON_NOTICE;
              2. Executeplayer_result.error = DUNGEON_PLAYER_INVALID_POSITION;
              3. Exit this functionreturn;
            False

            Continue after this decision.

          5. Decision!prepareDungeonExplored(&player_position, &explored_result)
            True
              1. Executestate = DUNGEON_NOTICE;
              2. Exit this functionreturn;
            False

            Continue after this decision.

          6. ExecutestartPlayTimer(now_ms);
          7. DecisionstartRoomEvent(&player_position)
            True
            1. Exit this functionreturn;
            False

            Continue after this decision.

          8. Executeimage_visible = false;
          9. Executedecode_pending = true;
        False
        1. Decisionselected_option == 1U
          True
            1. ExecutebeginDungeonCreation(false, now_ms);
            2. Exit this functionreturn;
          False
          1. Decisionselected_option == 2U
            True
              1. Executeai_mode = true;
              2. Executeai_start_at = now_ms;
              3. Executeai_start_seconds = 2U;
              4. Executestate = DUNGEON_AI_START;
            False
            1. Decisionselected_option == 3U
              True
                1. Decision!loadDungeonHistory(&history_result) && history_result.error == DUNGEON_HISTORY_OPEN_READ_FAILED
                  True
                  1. ExecuteinitDungeonHistory();
                  False

                  Continue after this decision.

                2. Executestate = DUNGEON_HISTORY;
              False
              1. Decisionselected_option == 4U
                True
                  1. Executemenu_back(now_ms);
                False
                  1. Executestate = DUNGEON_NOTICE;
    False
    1. Decisionstate == DUNGEON_NOTICE
      True
        1. Executestate = DUNGEON_START_MENU;
      False

      Continue after this decision.

Read original function source
void dungeonAction(bool action, bool map_action, bool status_action, bool context_action, uint32_t now_ms)
{
    if (!running) return;
    if (state == DUNGEON_COMPLETE && action) { menu_back(now_ms); return; }
    if (state == DUNGEON_HISTORY && action) { state = DUNGEON_START_MENU; return; }
    if (state == DUNGEON_SHOP) {
        if (status_action) { returnToRoom(); return; }
        if (action) applyShopChoice();
        return;
    }
    if (state == DUNGEON_LEVEL_BONUS) {
        if (status_action) { returnToRoom(); return; }
        if (action) {
            DungeonPlayerState *player = getDungeonPlayerState();
            const DungeonPlayerState previous_player = *player;
            if (applyDungeonPlayerStatChoice((uint8_t)selected_stat_option) && !saveDungeonPlayer(&player_result)) {
                *player = previous_player;
                state = DUNGEON_NOTICE;
            } else if (player->unspentStatPoints == 0U) returnToRoom();
        }
        return;
    }
    if (state == DUNGEON_LOCKED_DOOR && action) {
        DungeonPlayerState *player = getDungeonPlayerState();
        player->position = encounter_source;
        if (!saveDungeonPlayer(&player_result)) { state = DUNGEON_NOTICE; return; }
        player_position = encounter_source;
        returnToRoom();
        return;
    }
    if (state == DUNGEON_TRAP_RESULT && action) {
        if (!trap_defeated && startRoomEvent(&player_position)) return;
        returnToRoom();
        return;
    }
    if (state == DUNGEON_COMBAT_RESULT && action) {
        if (combat_victory && startRoomEvent(&player_position)) return;
        returnToRoom();
        return;
    }
    if (state == DUNGEON_COMBAT && action) { resolveCombatAction(); return; }
    if (state == DUNGEON_REWARD && action) {
        returnToRoom();
        return;
    }
    if (state == DUNGEON_ROOM && map_action) {
        if (map_visible) {
            map_visible = false;
            waiting_for_centre = true;
            returnToRoom();
            return;
        }
    }
    if (state == DUNGEON_ROOM && status_visible && context_action && status_page == 0U && getDungeonPlayerState()->unspentStatPoints > 0U) {
        status_visible = false;
        selected_stat_option = 0U;
        state = DUNGEON_LEVEL_BONUS;
        return;
    }
    if (state == DUNGEON_ROOM && !map_visible && !status_visible && context_action && isShopRoom(&getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x])) {
        selected_shop_option = 0U;
        shop_text[0] = '\0';
        state = DUNGEON_SHOP;
        image_visible = false;
        decode_pending = false;
        return;
    }
    if (state == DUNGEON_ROOM && status_action) {
        map_visible = false;
        status_visible = !status_visible;
        if (!status_visible) {
            image_visible = false;
            decode_pending = true;
        }
        waiting_for_centre = true;
        return;
    }
    if (state == DUNGEON_ROOM && status_visible) {
        if (action) status_page = (status_page + 1U) % 3U;
        return;
    }
    if (state == DUNGEON_ROOM && map_visible) {
        if (action) {
            if (map_page == 0U) map_page = 1U;
            else if (map_page == 1U) map_page = 3U;
            else if (map_page == 3U) map_page = 2U;
            else {
                map_page = 0U;
                map_floor = (map_floor + 1U) % DUNGEON_FLOOR_COUNT;
            }
        }
        waiting_for_centre = true;
        return;
    }
    if (state == DUNGEON_ROOM && map_action) {
        map_floor = player_position.floor;
        map_page = (player_position.y / 20U) * 2U + (player_position.x / 20U);
        map_visible = true;
        waiting_for_centre = true;
        return;
    }
    if (!action) return;
    if (state == DUNGEON_START_MENU && storage_result.error != DUNGEON_STORAGE_OK) {
        traceDungeonCreation("A RECEIVED", 0, 0);
        beginDungeonCreation(true, now_ms);
        return;
    }
    if (state == DUNGEON_START_MENU && storage_result.error == DUNGEON_STORAGE_OK) {
        if (selected_option == 0U) {
            if (!prepareDungeonPlayer(&player_result)) {
                state = DUNGEON_NOTICE;
                return;
            }
            state = DUNGEON_ROOM;
            player_position = getDungeonPlayerState()->position;
            if (!prepareRoomImage(&player_position)) {
                state = DUNGEON_NOTICE;
                player_result.error = DUNGEON_PLAYER_INVALID_POSITION;
                return;
            }
            if (!prepareDungeonExplored(&player_position, &explored_result)) {
                state = DUNGEON_NOTICE;
                return;
            }
            startPlayTimer(now_ms);
            if (startRoomEvent(&player_position)) return;
            image_visible = false;
            decode_pending = true;
        } else if (selected_option == 1U) {
            beginDungeonCreation(false, now_ms);
            return;
        } else if (selected_option == 2U) {
            ai_mode = true;
            ai_start_at = now_ms;
            ai_start_seconds = 2U;
            state = DUNGEON_AI_START;
        } else if (selected_option == 3U) {
            if (!loadDungeonHistory(&history_result) && history_result.error == DUNGEON_HISTORY_OPEN_READ_FAILED) initDungeonHistory();
            state = DUNGEON_HISTORY;
        } else if (selected_option == 4U) {
            menu_back(now_ms);
        } else {
            state = DUNGEON_NOTICE;
        }
    } else if (state == DUNGEON_NOTICE) {
        state = DUNGEON_START_MENU;
    }
}
dungeonAIControl() · source line 1052
void dungeonAIControl(bool speed_action, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running || !ai_mode || state != DUNGEON_ROOM || map_visible || status_visible
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionspeed_action
    True
      1. ExecutecycleAISpeed(&ai_speed_index);
      2. Executeai_last_move = now_ms;
    False

    Continue after this decision.

Read original function source
void dungeonAIControl(bool speed_action, uint32_t now_ms)
{
    if (!running || !ai_mode || state != DUNGEON_ROOM || map_visible || status_visible) return;
    if (speed_action) {
        cycleAISpeed(&ai_speed_index);
        ai_last_move = now_ms;
    }
}
updateDungeon() · source line 1064
void updateDungeon(uint16_t *frame, int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)

Calls into this documentation:

  1. Decision!running
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. ExecutecommitPlayTime(now_ms);
  3. Decisionstate == DUNGEON_AI_START
    True
      1. Decision!ai_mode
        True
          1. Executestate = DUNGEON_START_MENU;
          2. Exit this functionreturn;
        False

        Continue after this decision.

      2. Executeconst uint32_t elapsed = now_ms - ai_start_at;
      3. Decisionelapsed < 2000U
        True
          1. Executeai_start_seconds = (uint8_t)((2000U - elapsed + 999U) / 1000U);
          2. Exit this functionreturn;
        False

        Continue after this decision.

      4. Executestate = DUNGEON_START_MENU;
      5. Executeselected_option = 1U;
      6. ExecutedungeonAction(true, false, false, false, now_ms);
      7. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisionstate == DUNGEON_CREATING
    True
      1. ExecuteadvanceDungeonCreation(now_ms);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisiondecode_pending
    True
      1. Executedecode_pending = false;
      2. Executeimage_visible = imageDecodeFile(tile_path, frame);
    False

    Continue after this decision.

  6. Decisionai_mode && state == DUNGEON_ROOM && !map_visible && !status_visible
    True
      1. DecisionisShopRoom(&getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x]) && !ai_shop_visited
        True
          1. Executeai_shop_visited = true;
          2. ExecutedungeonAction(false, false, false, true, now_ms);
          3. Exit this functionreturn;
        False

        Continue after this decision.

      2. Decisionnow_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)
        True
          1. Executeai_last_move = now_ms;
          2. ExecuteaiMoveOnce();
        False

        Continue after this decision.

      3. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionai_mode && (state == DUNGEON_REWARD || state == DUNGEON_TRAP_RESULT)
    True
      1. Decisionnow_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)
        True
          1. Executeai_last_move = now_ms;
          2. ExecutedungeonAction(true, false, false, false, now_ms);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  8. Decisionai_mode && state == DUNGEON_COMBAT
    True
      1. Decisionnow_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)
        True
          1. Executeai_last_move = now_ms;
          2. ExecuteaiChooseCombatAction();
          3. ExecutedungeonAction(true, false, false, false, now_ms);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  9. Decisionai_mode && state == DUNGEON_COMBAT_RESULT
    True
      1. Decisionnow_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)
        True
          1. Executeai_last_move = now_ms;
          2. ExecutedungeonAction(true, false, false, false, now_ms);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  10. Decisionai_mode && state == DUNGEON_LOCKED_DOOR
    True
      1. Decisionnow_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)
        True
          1. Executeai_last_move = now_ms;
          2. ExecutedungeonAction(true, false, false, false, now_ms);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  11. Decisionai_mode && state == DUNGEON_SHOP
    True
      1. Decisionnow_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)
        True
          1. Executeai_last_move = now_ms;
          2. Executeselected_shop_option = aiChooseShopOption();
          3. Decisionselected_shop_option != 4U
            True
            1. Executeai_last_shop_option = selected_shop_option;
            False

            Continue after this decision.

          4. ExecutedungeonAction(true, false, selected_shop_option == 4U, false, now_ms);
        False

        Continue after this decision.

      2. Exit this functionreturn;
    False

    Continue after this decision.

  12. Decision(state != DUNGEON_START_MENU && state != DUNGEON_ROOM && state != DUNGEON_COMBAT && state != DUNGEON_LEVEL_BONUS && state != DUNGEON_SHOP) || !joystick_valid || map_visible || status_visible
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  13. Executeconst int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
  14. Executeconst int y_magnitude = y_offset < 0 ? -y_offset : y_offset;
  15. Decisionx_magnitude <= JOYSTICK_RELEASE && y_magnitude <= JOYSTICK_RELEASE
    True
      1. Executewaiting_for_centre = false;
      2. Executeheld_direction = 0;
      3. Executerepeating = false;
      4. Exit this functionreturn;
    False

    Continue after this decision.

  16. Decisionwaiting_for_centre
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  17. Decisionstate == DUNGEON_ROOM
    True
      1. Decisionx_magnitude < JOYSTICK_ENTER && y_magnitude < JOYSTICK_ENTER
        True
        1. Exit this functionreturn;
        False

        Continue after this decision.

      2. Decisionx_offset >= JOYSTICK_ENTER && y_magnitude >= JOYSTICK_ENTER
        True
          1. ExecutemoveStairs(y_offset > 0);
          2. Executewaiting_for_centre = true;
          3. Exit this functionreturn;
        False

        Continue after this decision.

      3. Executeuint8_t exit;
      4. Decisionx_magnitude >= y_magnitude
        True
        1. Executeexit = x_offset > 0 ? DUNGEON_EXIT_EAST : DUNGEON_EXIT_WEST;
        False
        1. Executeexit = y_offset > 0 ? DUNGEON_EXIT_NORTH : DUNGEON_EXIT_SOUTH;
      5. ExecutemovePlayer(exit);
      6. Executewaiting_for_centre = true;
      7. Exit this functionreturn;
    False

    Continue after this decision.

  18. Decisiony_magnitude < JOYSTICK_ENTER
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  19. Executeconst int direction = y_offset > 0 ? -1 : 1;
  20. Decisiondirection == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  21. Executerepeating = direction == held_direction;
  22. Executeheld_direction = direction;
  23. Executelast_move = now_ms;
  24. Decisionstate == DUNGEON_COMBAT
    True
      1. Executeconst unsigned count = combatOptionCount();
      2. Decisiondirection < 0 && selected_combat_option > 0U
        True
        1. Execute--selected_combat_option;
        False

        Continue after this decision.

      3. Decisiondirection > 0 && selected_combat_option + 1U < count
        True
        1. Execute++selected_combat_option;
        False

        Continue after this decision.

      4. Exit this functionreturn;
    False

    Continue after this decision.

  25. Decisionstate == DUNGEON_LEVEL_BONUS
    True
      1. Decisiondirection < 0 && selected_stat_option > 0U
        True
        1. Execute--selected_stat_option;
        False

        Continue after this decision.

      2. Decisiondirection > 0 && selected_stat_option < 2U
        True
        1. Execute++selected_stat_option;
        False

        Continue after this decision.

      3. Exit this functionreturn;
    False

    Continue after this decision.

  26. Decisionstate == DUNGEON_SHOP
    True
      1. Decisiondirection < 0 && selected_shop_option > 0U
        True
        1. Execute--selected_shop_option;
        False

        Continue after this decision.

      2. Decisiondirection > 0 && selected_shop_option < 4U
        True
        1. Execute++selected_shop_option;
        False

        Continue after this decision.

      3. Exit this functionreturn;
    False

    Continue after this decision.

  27. Executeconst unsigned count = sizeof(start_options) / sizeof(start_options[0]);
  28. Decisiondirection < 0 && selected_option > 0U
    True
    1. Execute--selected_option;
    False

    Continue after this decision.

  29. Decisiondirection > 0 && selected_option + 1U < count
    True
    1. Execute++selected_option;
    False

    Continue after this decision.

Read original function source
void updateDungeon(uint16_t *frame, int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
{
    if (!running) return;
    commitPlayTime(now_ms);
    if (state == DUNGEON_AI_START) {
        if (!ai_mode) {
            state = DUNGEON_START_MENU;
            return;
        }
        const uint32_t elapsed = now_ms - ai_start_at;
        if (elapsed < 2000U) {
            ai_start_seconds = (uint8_t)((2000U - elapsed + 999U) / 1000U);
            return;
        }
        state = DUNGEON_START_MENU;
        selected_option = 1U;
        dungeonAction(true, false, false, false, now_ms);
        return;
    }
    if (state == DUNGEON_CREATING) {
        advanceDungeonCreation(now_ms);
        return;
    }
    if (decode_pending) {
        decode_pending = false;
        image_visible = imageDecodeFile(tile_path, frame);
    }
    if (ai_mode && state == DUNGEON_ROOM && !map_visible && !status_visible) {
        if (isShopRoom(&getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x]) && !ai_shop_visited) {
            ai_shop_visited = true;
            dungeonAction(false, false, false, true, now_ms);
            return;
        }
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            aiMoveOnce();
        }
        return;
    }
    if (ai_mode && (state == DUNGEON_REWARD || state == DUNGEON_TRAP_RESULT)) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            dungeonAction(true, false, false, false, now_ms);
        }
        return;
    }
    if (ai_mode && state == DUNGEON_COMBAT) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            aiChooseCombatAction();
            dungeonAction(true, false, false, false, now_ms);
        }
        return;
    }
    if (ai_mode && state == DUNGEON_COMBAT_RESULT) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            dungeonAction(true, false, false, false, now_ms);
        }
        return;
    }
    if (ai_mode && state == DUNGEON_LOCKED_DOOR) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            dungeonAction(true, false, false, false, now_ms);
        }
        return;
    }
    if (ai_mode && state == DUNGEON_SHOP) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            selected_shop_option = aiChooseShopOption();
            if (selected_shop_option != 4U) ai_last_shop_option = selected_shop_option;
            dungeonAction(true, false, selected_shop_option == 4U, false, now_ms);
        }
        return;
    }
    if ((state != DUNGEON_START_MENU && state != DUNGEON_ROOM && state != DUNGEON_COMBAT && state != DUNGEON_LEVEL_BONUS && state != DUNGEON_SHOP) || !joystick_valid || map_visible || status_visible) return;
    const int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
    const int y_magnitude = y_offset < 0 ? -y_offset : y_offset;
    if (x_magnitude <= JOYSTICK_RELEASE && y_magnitude <= JOYSTICK_RELEASE) {
        waiting_for_centre = false;
        held_direction = 0;
        repeating = false;
        return;
    }
    if (waiting_for_centre) return;

    if (state == DUNGEON_ROOM) {
        if (x_magnitude < JOYSTICK_ENTER && y_magnitude < JOYSTICK_ENTER) return;
        if (x_offset >= JOYSTICK_ENTER && y_magnitude >= JOYSTICK_ENTER) {
            moveStairs(y_offset > 0);
            waiting_for_centre = true;
            return;
        }
        uint8_t exit;
        if (x_magnitude >= y_magnitude) exit = x_offset > 0 ? DUNGEON_EXIT_EAST : DUNGEON_EXIT_WEST;
        else exit = y_offset > 0 ? DUNGEON_EXIT_NORTH : DUNGEON_EXIT_SOUTH;
        movePlayer(exit);
        waiting_for_centre = true;
        return;
    }

    if (y_magnitude < JOYSTICK_ENTER) return;
    const int direction = y_offset > 0 ? -1 : 1;
    if (direction == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)) return;
    repeating = direction == held_direction;
    held_direction = direction;
    last_move = now_ms;
    if (state == DUNGEON_COMBAT) {
        const unsigned count = combatOptionCount();
        if (direction < 0 && selected_combat_option > 0U) --selected_combat_option;
        if (direction > 0 && selected_combat_option + 1U < count) ++selected_combat_option;
        return;
    }
    if (state == DUNGEON_LEVEL_BONUS) {
        if (direction < 0 && selected_stat_option > 0U) --selected_stat_option;
        if (direction > 0 && selected_stat_option < 2U) ++selected_stat_option;
        return;
    }
    if (state == DUNGEON_SHOP) {
        if (direction < 0 && selected_shop_option > 0U) --selected_shop_option;
        if (direction > 0 && selected_shop_option < 4U) ++selected_shop_option;
        return;
    }
    const unsigned count = sizeof(start_options) / sizeof(start_options[0]);
    if (direction < 0 && selected_option > 0U) --selected_option;
    if (direction > 0 && selected_option + 1U < count) ++selected_option;
}
drawBoldLine() · source line 1197
static void drawBoldLine(uint16_t *frame, int x0, int y0, int x1, int y1, uint16_t color)

Calls into this documentation:

  1. Executedraw_line(frame, x0, y0, x1, y1, color);
  2. Executedraw_line(frame, x0 + 1, y0, x1 + 1, y1, color);
  3. Executedraw_line(frame, x0, y0 + 1, x1, y1 + 1, color);
Read original function source
static void drawBoldLine(uint16_t *frame, int x0, int y0, int x1, int y1, uint16_t color)
{
    draw_line(frame, x0, y0, x1, y1, color);
    draw_line(frame, x0 + 1, y0, x1 + 1, y1, color);
    draw_line(frame, x0, y0 + 1, x1, y1 + 1, color);
}
exitDestinationExplored() · source line 1204
static bool exitDestinationExplored(uint8_t exit)

Calls into this documentation:

  1. Executeint x = player_position.x;
  2. Executeint y = player_position.y;
  3. Decisionexit == DUNGEON_EXIT_NORTH
    True
    1. Execute--y;
    False
    1. Decisionexit == DUNGEON_EXIT_EAST
      True
      1. Execute++x;
      False
      1. Decisionexit == DUNGEON_EXIT_SOUTH
        True
        1. Execute++y;
        False
        1. Decisionexit == DUNGEON_EXIT_WEST
          True
          1. Execute--x;
          False

          Continue after this decision.

  4. Exit this functionreturn x >= 0 && x < (int)DUNGEON_WIDTH && y >= 0 && y < (int)DUNGEON_HEIGHT && isDungeonExplored(player_position.floor, (uint8_t)x, (uint8_t)y);
Read original function source
static bool exitDestinationExplored(uint8_t exit)
{
    int x = player_position.x;
    int y = player_position.y;
    if (exit == DUNGEON_EXIT_NORTH) --y;
    else if (exit == DUNGEON_EXIT_EAST) ++x;
    else if (exit == DUNGEON_EXIT_SOUTH) ++y;
    else if (exit == DUNGEON_EXIT_WEST) --x;
    return x >= 0 && x < (int)DUNGEON_WIDTH && y >= 0 && y < (int)DUNGEON_HEIGHT &&
           isDungeonExplored(player_position.floor, (uint8_t)x, (uint8_t)y);
}
drawExitIcon() · source line 1216
static void drawExitIcon(uint16_t *frame, uint8_t exit, uint16_t color)

Calls into this documentation:

  1. Executeint x = 160;
  2. Executeint y = 85;
  3. Decisionexit == DUNGEON_EXIT_NORTH
    True
    1. Executey = 12;
    False
    1. Decisionexit == DUNGEON_EXIT_EAST
      True
      1. Executex = LCD_WIDTH - 13;
      False
      1. Decisionexit == DUNGEON_EXIT_SOUTH
        True
        1. Executey = 157;
        False
        1. Decisionexit == DUNGEON_EXIT_WEST
          True
          1. Executex = 12;
          False

          Continue after this decision.

  4. Decisionexit == DUNGEON_EXIT_NORTH || exit == DUNGEON_EXIT_SOUTH
    True
      1. Executeconst int direction = exit == DUNGEON_EXIT_NORTH ? -1 : 1;
      2. ExecutedrawBoldLine(frame, x, y - direction * 7, x, y + direction * 7, color);
      3. ExecutedrawBoldLine(frame, x, y + direction * 7, x - 5, y + direction * 2, color);
      4. ExecutedrawBoldLine(frame, x, y + direction * 7, x + 5, y + direction * 2, color);
    False
      1. Executeconst int direction = exit == DUNGEON_EXIT_EAST ? 1 : -1;
      2. ExecutedrawBoldLine(frame, x - direction * 7, y, x + direction * 7, y, color);
      3. ExecutedrawBoldLine(frame, x + direction * 7, y, x + direction * 2, y - 5, color);
      4. ExecutedrawBoldLine(frame, x + direction * 7, y, x + direction * 2, y + 5, color);
Read original function source
static void drawExitIcon(uint16_t *frame, uint8_t exit, uint16_t color)
{
    int x = 160;
    int y = 85;
    if (exit == DUNGEON_EXIT_NORTH) y = 12;
    else if (exit == DUNGEON_EXIT_EAST) x = LCD_WIDTH - 13;
    else if (exit == DUNGEON_EXIT_SOUTH) y = 157;
    else if (exit == DUNGEON_EXIT_WEST) x = 12;

    if (exit == DUNGEON_EXIT_NORTH || exit == DUNGEON_EXIT_SOUTH) {
        const int direction = exit == DUNGEON_EXIT_NORTH ? -1 : 1;
        drawBoldLine(frame, x, y - direction * 7, x, y + direction * 7, color);
        drawBoldLine(frame, x, y + direction * 7, x - 5, y + direction * 2, color);
        drawBoldLine(frame, x, y + direction * 7, x + 5, y + direction * 2, color);
    } else {
        const int direction = exit == DUNGEON_EXIT_EAST ? 1 : -1;
        drawBoldLine(frame, x - direction * 7, y, x + direction * 7, y, color);
        drawBoldLine(frame, x + direction * 7, y, x + direction * 2, y - 5, color);
        drawBoldLine(frame, x + direction * 7, y, x + direction * 2, y + 5, color);
    }
}
drawStairIcon() · source line 1238
static void drawStairIcon(uint16_t *frame, bool up)

Calls into this documentation:

  1. Executeconst int x = LCD_WIDTH - 44;
  2. Executeconst int y = up ? 12 : 157;
  3. Executeconst int direction = up ? -1 : 1;
  4. ExecutedrawBoldLine(frame, x, y - direction * 7, x, y + direction * 7, COLOR_YELLOW);
  5. ExecutedrawBoldLine(frame, x, y + direction * 7, x - 5, y + direction * 2, COLOR_YELLOW);
  6. ExecutedrawBoldLine(frame, x, y + direction * 7, x + 5, y + direction * 2, COLOR_YELLOW);
Read original function source
static void drawStairIcon(uint16_t *frame, bool up)
{
    const int x = LCD_WIDTH - 44;
    const int y = up ? 12 : 157;
    const int direction = up ? -1 : 1;
    drawBoldLine(frame, x, y - direction * 7, x, y + direction * 7, COLOR_YELLOW);
    drawBoldLine(frame, x, y + direction * 7, x - 5, y + direction * 2, COLOR_YELLOW);
    drawBoldLine(frame, x, y + direction * 7, x + 5, y + direction * 2, COLOR_YELLOW);
}
drawShopIndicator() · source line 1248
static void drawShopIndicator(uint16_t *frame)

Calls into this documentation:

  1. Executedraw_text(frame, 5, 5, "C SHOP", 1, COLOR_YELLOW);
Read original function source
static void drawShopIndicator(uint16_t *frame)
{
    draw_text(frame, 5, 5, "C SHOP", 1, COLOR_YELLOW);
}
mapCellColor() · source line 1253
static uint16_t mapCellColor(const DungeonCell *cell)
  1. Decisioncell->tileType == DUNGEON_TILE_STAIRS_UP || cell->tileType == DUNGEON_TILE_STAIRS_DOWN || cell->tileType == DUNGEON_TILE_STAIRS_BOTH
    True
    1. Exit this functionreturn COLOR_PURPLE;
    False

    Continue after this decision.

  2. Decisioncell->tileType == DUNGEON_TILE_ENTRANCE
    True
    1. Exit this functionreturn COLOR_GREEN;
    False

    Continue after this decision.

  3. Decisioncell->tileType == DUNGEON_TILE_EXIT
    True
    1. Exit this functionreturn COLOR_MAGENTA;
    False

    Continue after this decision.

  4. Decisioncell->tileType == DUNGEON_TILE_PASSAGE || cell->tileType == DUNGEON_TILE_DEAD_END_PASSAGE
    True
    1. Exit this functionreturn COLOR_LIGHTGRAY;
    False

    Continue after this decision.

  5. Exit this functionreturn COLOR_CYAN;
Read original function source
static uint16_t mapCellColor(const DungeonCell *cell)
{
    if (cell->tileType == DUNGEON_TILE_STAIRS_UP || cell->tileType == DUNGEON_TILE_STAIRS_DOWN || cell->tileType == DUNGEON_TILE_STAIRS_BOTH) return COLOR_PURPLE;
    if (cell->tileType == DUNGEON_TILE_ENTRANCE) return COLOR_GREEN;
    if (cell->tileType == DUNGEON_TILE_EXIT) return COLOR_MAGENTA;
    if (cell->tileType == DUNGEON_TILE_PASSAGE || cell->tileType == DUNGEON_TILE_DEAD_END_PASSAGE) return COLOR_LIGHTGRAY;
    return COLOR_CYAN;
}
drawMapCell() · source line 1265
static void drawMapCell(uint16_t *frame, unsigned x, unsigned y, unsigned first_column, unsigned first_row, const DungeonCell *cell)

Calls into this documentation:

  1. Executeconst int screen_x = ((int)x - (int)first_column) * 8;
  2. Executeconst int screen_y = ((int)y - (int)first_row) * 8;
  3. Executeconst uint16_t color = mapCellColor(cell);
  4. Executeconst bool large = cell->tileType != DUNGEON_TILE_PASSAGE && cell->tileType != DUNGEON_TILE_DEAD_END_PASSAGE;
  5. Executefill_rect(frame, screen_x + (large ? 1 : 2), screen_y + (large ? 1 : 2), large ? 6 : 4, large ? 6 : 4, color);
  6. Decision(cell->exits & DUNGEON_EXIT_NORTH) != 0U
    True
      1. Executeconst bool known = y > 0U && isDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)(y - 1U));
      2. Executefill_rect(frame, screen_x + 3, screen_y, 2, 4, known ? color : COLOR_ORANGE);
      3. Decision!known
        True
        1. Executefill_rect(frame, screen_x + 2, screen_y, 4, 2, COLOR_ORANGE);
        False

        Continue after this decision.

    False

    Continue after this decision.

  7. Decision(cell->exits & DUNGEON_EXIT_EAST) != 0U
    True
      1. Executeconst bool known = x + 1U < DUNGEON_WIDTH && isDungeonExplored((uint8_t)map_floor, (uint8_t)(x + 1U), (uint8_t)y);
      2. Executefill_rect(frame, screen_x + 4, screen_y + 3, 4, 2, known ? color : COLOR_ORANGE);
      3. Decision!known
        True
        1. Executefill_rect(frame, screen_x + 6, screen_y + 2, 2, 4, COLOR_ORANGE);
        False

        Continue after this decision.

    False

    Continue after this decision.

  8. Decision(cell->exits & DUNGEON_EXIT_SOUTH) != 0U
    True
      1. Executeconst bool known = y + 1U < DUNGEON_HEIGHT && isDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)(y + 1U));
      2. Executefill_rect(frame, screen_x + 3, screen_y + 4, 2, 4, known ? color : COLOR_ORANGE);
      3. Decision!known
        True
        1. Executefill_rect(frame, screen_x + 2, screen_y + 6, 4, 2, COLOR_ORANGE);
        False

        Continue after this decision.

    False

    Continue after this decision.

  9. Decision(cell->exits & DUNGEON_EXIT_WEST) != 0U
    True
      1. Executeconst bool known = x > 0U && isDungeonExplored((uint8_t)map_floor, (uint8_t)(x - 1U), (uint8_t)y);
      2. Executefill_rect(frame, screen_x, screen_y + 3, 4, 2, known ? color : COLOR_ORANGE);
      3. Decision!known
        True
        1. Executefill_rect(frame, screen_x, screen_y + 2, 2, 4, COLOR_ORANGE);
        False

        Continue after this decision.

    False

    Continue after this decision.

Read original function source
static void drawMapCell(uint16_t *frame, unsigned x, unsigned y, unsigned first_column, unsigned first_row, const DungeonCell *cell)
{
    const int screen_x = ((int)x - (int)first_column) * 8;
    const int screen_y = ((int)y - (int)first_row) * 8;
    const uint16_t color = mapCellColor(cell);
    const bool large = cell->tileType != DUNGEON_TILE_PASSAGE && cell->tileType != DUNGEON_TILE_DEAD_END_PASSAGE;
    fill_rect(frame, screen_x + (large ? 1 : 2), screen_y + (large ? 1 : 2), large ? 6 : 4, large ? 6 : 4, color);
    if ((cell->exits & DUNGEON_EXIT_NORTH) != 0U) {
        const bool known = y > 0U && isDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)(y - 1U));
        fill_rect(frame, screen_x + 3, screen_y, 2, 4, known ? color : COLOR_ORANGE);
        if (!known) fill_rect(frame, screen_x + 2, screen_y, 4, 2, COLOR_ORANGE);
    }
    if ((cell->exits & DUNGEON_EXIT_EAST) != 0U) {
        const bool known = x + 1U < DUNGEON_WIDTH && isDungeonExplored((uint8_t)map_floor, (uint8_t)(x + 1U), (uint8_t)y);
        fill_rect(frame, screen_x + 4, screen_y + 3, 4, 2, known ? color : COLOR_ORANGE);
        if (!known) fill_rect(frame, screen_x + 6, screen_y + 2, 2, 4, COLOR_ORANGE);
    }
    if ((cell->exits & DUNGEON_EXIT_SOUTH) != 0U) {
        const bool known = y + 1U < DUNGEON_HEIGHT && isDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)(y + 1U));
        fill_rect(frame, screen_x + 3, screen_y + 4, 2, 4, known ? color : COLOR_ORANGE);
        if (!known) fill_rect(frame, screen_x + 2, screen_y + 6, 4, 2, COLOR_ORANGE);
    }
    if ((cell->exits & DUNGEON_EXIT_WEST) != 0U) {
        const bool known = x > 0U && isDungeonExplored((uint8_t)map_floor, (uint8_t)(x - 1U), (uint8_t)y);
        fill_rect(frame, screen_x, screen_y + 3, 4, 2, known ? color : COLOR_ORANGE);
        if (!known) fill_rect(frame, screen_x, screen_y + 2, 2, 4, COLOR_ORANGE);
    }
}
drawMapKeyItem() · source line 1294
static void drawMapKeyItem(uint16_t *frame, int y, uint16_t color, const char *label)

Calls into this documentation:

  1. Executefill_rect(frame, 174, y, 10, 10, color);
  2. ExecutedrawPet2001TextTransparent(frame, 190, y + 1, label, COLOR_WHITE, 1);
Read original function source
static void drawMapKeyItem(uint16_t *frame, int y, uint16_t color, const char *label)
{
    fill_rect(frame, 174, y, 10, 10, color);
    drawPet2001TextTransparent(frame, 190, y + 1, label, COLOR_WHITE, 1);
}
drawMapSidePanel() · source line 1300
static void drawMapSidePanel(uint16_t *frame)

Calls into this documentation:

  1. Executedraw_line(frame, 164, 0, 164, 160, COLOR_DARKGRAY);
  2. ExecutedrawPet2001TextTransparent(frame, 174, 5, "KEY", COLOR_CYAN, 2);
  3. ExecutedrawMapKeyItem(frame, 31, COLOR_YELLOW, "PLR");
  4. ExecutedrawMapKeyItem(frame, 47, COLOR_LIGHTGRAY, "PATH");
  5. ExecutedrawMapKeyItem(frame, 63, COLOR_CYAN, "ROOM");
  6. ExecutedrawMapKeyItem(frame, 79, COLOR_ORANGE, "UNEXP EXIT");
  7. ExecutedrawMapKeyItem(frame, 95, COLOR_PURPLE, "STAIRS");
  8. ExecutedrawMapKeyItem(frame, 111, COLOR_GREEN, "ENTRANCE");
  9. ExecutedrawMapKeyItem(frame, 127, COLOR_RED, "KEY");
  10. ExecutedrawPet2001TextCentredTransparent(frame, 247, 3, "N", COLOR_WHITE, 1);
  11. ExecutedrawPet2001TextCentredTransparent(frame, 247, 35, "S", COLOR_WHITE, 1);
  12. ExecutedrawPet2001TextCentredTransparent(frame, 232, 19, "W", COLOR_WHITE, 1);
  13. ExecutedrawPet2001TextCentredTransparent(frame, 262, 19, "E", COLOR_WHITE, 1);
  14. Executedraw_line(frame, 239, 23, 255, 23, COLOR_LIGHTGRAY);
  15. Executedraw_line(frame, 247, 15, 247, 31, COLOR_LIGHTGRAY);
  16. Executedraw_line(frame, 247, 13, 243, 20, COLOR_RED);
  17. Executedraw_line(frame, 247, 13, 251, 20, COLOR_RED);
  18. Executedraw_rect(frame, 270, 5, 40, 40, COLOR_WHITE);
  19. Executedraw_line(frame, 290, 5, 290, 45, COLOR_WHITE);
  20. Executedraw_line(frame, 270, 25, 310, 25, COLOR_WHITE);
  21. FOR loopunsigned page = 0; page < 4U; ++page

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst int x = 270 + (int)(page % 2U) * 20;
      2. Executeconst int y = 5 + (int)(page / 2U) * 20;
      3. Executeconst bool selected = page == map_page;
      4. Executefill_rect(frame, x + 2, y + 2, 17, 17, selected ? COLOR_YELLOW : COLOR_DARKGRAY);
      5. Executechar number[2] = { (char)('1' + page), '\0' };
      6. ExecutedrawPet2001TextCentredTransparent(frame, x + 10, y + 6, number, selected ? COLOR_BLACK : COLOR_WHITE, 1);
  22. Executeconst unsigned player_page = (player_position.y / 20U) * 2U + player_position.x / 20U;
  23. Decisionplayer_position.floor == map_floor && player_page != map_page
    True
      1. Executeconst int x = 270 + (int)(player_page % 2U) * 20;
      2. Executeconst int y = 5 + (int)(player_page / 2U) * 20;
      3. Executefill_circle(frame, x + 15, y + 15, 2, COLOR_YELLOW);
    False

    Continue after this decision.

Read original function source
static void drawMapSidePanel(uint16_t *frame)
{
    draw_line(frame, 164, 0, 164, 160, COLOR_DARKGRAY);
    drawPet2001TextTransparent(frame, 174, 5, "KEY", COLOR_CYAN, 2);
    drawMapKeyItem(frame, 31, COLOR_YELLOW, "PLR");
    drawMapKeyItem(frame, 47, COLOR_LIGHTGRAY, "PATH");
    drawMapKeyItem(frame, 63, COLOR_CYAN, "ROOM");
    drawMapKeyItem(frame, 79, COLOR_ORANGE, "UNEXP EXIT");
    drawMapKeyItem(frame, 95, COLOR_PURPLE, "STAIRS");
    drawMapKeyItem(frame, 111, COLOR_GREEN, "ENTRANCE");
    drawMapKeyItem(frame, 127, COLOR_RED, "KEY");

    drawPet2001TextCentredTransparent(frame, 247, 3, "N", COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 247, 35, "S", COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 232, 19, "W", COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 262, 19, "E", COLOR_WHITE, 1);
    draw_line(frame, 239, 23, 255, 23, COLOR_LIGHTGRAY);
    draw_line(frame, 247, 15, 247, 31, COLOR_LIGHTGRAY);
    draw_line(frame, 247, 13, 243, 20, COLOR_RED);
    draw_line(frame, 247, 13, 251, 20, COLOR_RED);

    draw_rect(frame, 270, 5, 40, 40, COLOR_WHITE);
    draw_line(frame, 290, 5, 290, 45, COLOR_WHITE);
    draw_line(frame, 270, 25, 310, 25, COLOR_WHITE);
    for (unsigned page = 0; page < 4U; ++page) {
        const int x = 270 + (int)(page % 2U) * 20;
        const int y = 5 + (int)(page / 2U) * 20;
        const bool selected = page == map_page;
        fill_rect(frame, x + 2, y + 2, 17, 17, selected ? COLOR_YELLOW : COLOR_DARKGRAY);
        char number[2] = { (char)('1' + page), '\0' };
        drawPet2001TextCentredTransparent(frame, x + 10, y + 6, number,
                                           selected ? COLOR_BLACK : COLOR_WHITE, 1);
    }
    const unsigned player_page = (player_position.y / 20U) * 2U + player_position.x / 20U;
    if (player_position.floor == map_floor && player_page != map_page) {
        const int x = 270 + (int)(player_page % 2U) * 20;
        const int y = 5 + (int)(player_page / 2U) * 20;
        fill_circle(frame, x + 15, y + 15, 2, COLOR_YELLOW);
    }
}
drawDungeonMap() · source line 1341
static void drawDungeonMap(uint16_t *frame)

Calls into this documentation:

  1. Executeconst unsigned first_column = (map_page % 2U) * 20U;
  2. Executeconst unsigned first_row = (map_page / 2U) * 20U;
  3. Executeconst DungeonFloor *floor = &getDungeonWorld()->floors[map_floor];
  4. Executeclear(frame, COLOR_BLACK);
  5. FOR loopunsigned y = first_row; y < first_row + 20U; ++y

    Repeat body while the loop condition permits; then continue below.

    1. FOR loopunsigned x = first_column; x < first_column + 20U; ++x

      Repeat body while the loop condition permits; then continue below.

      1. DecisionisDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)y)
        True
        1. ExecutedrawMapCell(frame, x, y, first_column, first_row, &floor->cells[y][x]);
        False

        Continue after this decision.

  6. Decision(getDungeonPlayerState()->runFlags & DUNGEON_RUN_KEY_REVEALED) != 0U
    True
      1. Executebool key_drawn = false;
      2. FOR loopunsigned y = 0; y < DUNGEON_HEIGHT && !key_drawn; ++y

        Repeat body while the loop condition permits; then continue below.

          1. FOR loopunsigned x = 0; x < DUNGEON_WIDTH; ++x

            Repeat body while the loop condition permits; then continue below.

              1. Executeconst DungeonCell *cell = &floor->cells[y][x];
              2. Decision(cell->eventFlags & DUNGEON_EVENT_KEY) == 0U || (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) != 0U
                True
                1. Next loop iterationcontinue;
                False

                Continue after this decision.

              3. Decisionx >= first_column && x < first_column + 20U && y >= first_row && y < first_row + 20U
                True
                1. Executefill_rect(frame, ((int)x - (int)first_column) * 8 + 1, ((int)y - (int)first_row) * 8 + 1, 6, 6, COLOR_RED);
                False

                Continue after this decision.

              4. Executekey_drawn = true;
              5. Exit enclosing loop or switchbreak;
    False

    Continue after this decision.

  7. Decisionplayer_position.floor == map_floor && player_position.x >= first_column && player_position.x < first_column + 20U && player_position.y >= first_row && player_position.y < first_row + 20U
    True
    1. Executefill_rect(frame, ((int)player_position.x - (int)first_column) * 8 + 1, ((int)player_position.y - (int)first_row) * 8 + 1, 6, 6, COLOR_YELLOW);
    False

    Continue after this decision.

  8. ExecutedrawMapSidePanel(frame);
  9. Executechar footer[20];
  10. Executesnprintf(footer, sizeof(footer), "FLOOR %u PAGE %u/4", map_floor + 1U, map_page + 1U);
  11. ExecutedrawPet2001TextTransparent(frame, 4, 161, footer, COLOR_WHITE, 1);
  12. ExecutedrawPet2001TextTransparent(frame, 172, 161, "A/K PAGE C CLOSE", COLOR_LIGHTGRAY, 1);
Read original function source
static void drawDungeonMap(uint16_t *frame)
{
    const unsigned first_column = (map_page % 2U) * 20U;
    const unsigned first_row = (map_page / 2U) * 20U;
    const DungeonFloor *floor = &getDungeonWorld()->floors[map_floor];
    clear(frame, COLOR_BLACK);
    for (unsigned y = first_row; y < first_row + 20U; ++y)
        for (unsigned x = first_column; x < first_column + 20U; ++x)
            if (isDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)y)) drawMapCell(frame, x, y, first_column, first_row, &floor->cells[y][x]);
    if ((getDungeonPlayerState()->runFlags & DUNGEON_RUN_KEY_REVEALED) != 0U) {
        bool key_drawn = false;
        for (unsigned y = 0; y < DUNGEON_HEIGHT && !key_drawn; ++y) {
            for (unsigned x = 0; x < DUNGEON_WIDTH; ++x) {
                const DungeonCell *cell = &floor->cells[y][x];
                if ((cell->eventFlags & DUNGEON_EVENT_KEY) == 0U ||
                    (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) != 0U) continue;
                if (x >= first_column && x < first_column + 20U &&
                    y >= first_row && y < first_row + 20U)
                    fill_rect(frame, ((int)x - (int)first_column) * 8 + 1,
                              ((int)y - (int)first_row) * 8 + 1, 6, 6, COLOR_RED);
                key_drawn = true;
                break;
            }
        }
    }
    if (player_position.floor == map_floor && player_position.x >= first_column && player_position.x < first_column + 20U && player_position.y >= first_row && player_position.y < first_row + 20U)
        fill_rect(frame, ((int)player_position.x - (int)first_column) * 8 + 1, ((int)player_position.y - (int)first_row) * 8 + 1, 6, 6, COLOR_YELLOW);
    drawMapSidePanel(frame);
    char footer[20];
    snprintf(footer, sizeof(footer), "FLOOR %u  PAGE %u/4", map_floor + 1U, map_page + 1U);
    drawPet2001TextTransparent(frame, 4, 161, footer, COLOR_WHITE, 1);
    drawPet2001TextTransparent(frame, 172, 161, "A/K PAGE  C CLOSE", COLOR_LIGHTGRAY, 1);
}
isRoomTile() · source line 1375
static bool isRoomTile(uint8_t tile_type)
  1. Exit this functionreturn tile_type == DUNGEON_TILE_ROOM_OPEN || tile_type == DUNGEON_TILE_DEAD_END_ROOM_ROUND || tile_type == DUNGEON_TILE_DEAD_END_ROOM_SQUARE || tile_type == DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT;
Read original function source
static bool isRoomTile(uint8_t tile_type)
{
    return tile_type == DUNGEON_TILE_ROOM_OPEN ||
           tile_type == DUNGEON_TILE_DEAD_END_ROOM_ROUND ||
           tile_type == DUNGEON_TILE_DEAD_END_ROOM_SQUARE ||
           tile_type == DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT;
}
exploredRoomCount() · source line 1383
static unsigned exploredRoomCount(unsigned floor_index, unsigned *total_rooms)

Calls into this documentation:

  1. Executeunsigned explored = 0;
  2. Executeunsigned total = 0;
  3. FOR loopunsigned y = 0; y < DUNGEON_HEIGHT; ++y

    Repeat body while the loop condition permits; then continue below.

    1. FOR loopunsigned x = 0; x < DUNGEON_WIDTH; ++x

      Repeat body while the loop condition permits; then continue below.

      1. DecisionisRoomTile(getDungeonWorld()->floors[floor_index].cells[y][x].tileType)
        True
          1. Execute++total;
          2. DecisionisDungeonExplored((uint8_t)floor_index, (uint8_t)x, (uint8_t)y)
            True
            1. Execute++explored;
            False

            Continue after this decision.

        False

        Continue after this decision.

  4. Decisiontotal_rooms != NULL
    True
    1. Execute*total_rooms = total;
    False

    Continue after this decision.

  5. Exit this functionreturn explored;
Read original function source
static unsigned exploredRoomCount(unsigned floor_index, unsigned *total_rooms)
{
    unsigned explored = 0;
    unsigned total = 0;
    for (unsigned y = 0; y < DUNGEON_HEIGHT; ++y)
        for (unsigned x = 0; x < DUNGEON_WIDTH; ++x)
            if (isRoomTile(getDungeonWorld()->floors[floor_index].cells[y][x].tileType)) {
                ++total;
                if (isDungeonExplored((uint8_t)floor_index, (uint8_t)x, (uint8_t)y)) ++explored;
            }
    if (total_rooms != NULL) *total_rooms = total;
    return explored;
}
specialItemCount() · source line 1397
static unsigned specialItemCount(uint32_t flags)
  1. Executeunsigned count = 0;
  2. WHILE loopflags != 0U

    Repeat body while the loop condition permits; then continue below.

      1. Executecount += flags & 1U;
      2. Executeflags >>= 1;
  3. Exit this functionreturn count;
Read original function source
static unsigned specialItemCount(uint32_t flags)
{
    unsigned count = 0;
    while (flags != 0U) {
        count += flags & 1U;
        flags >>= 1;
    }
    return count;
}
formatPlayedTime() · source line 1407
static void formatPlayedTime(char *text, size_t size, uint32_t total_seconds)
  1. Executeconst uint32_t hours = total_seconds / 3600U;
  2. Executeconst uint32_t minutes = (total_seconds / 60U) % 60U;
  3. Executeconst uint32_t seconds = total_seconds % 60U;
  4. Executesnprintf(text, size, "%lu:%02lu:%02lu", (unsigned long)hours, (unsigned long)minutes, (unsigned long)seconds);
Read original function source
static void formatPlayedTime(char *text, size_t size, uint32_t total_seconds)
{
    const uint32_t hours = total_seconds / 3600U;
    const uint32_t minutes = (total_seconds / 60U) % 60U;
    const uint32_t seconds = total_seconds % 60U;
    snprintf(text, size, "%lu:%02lu:%02lu", (unsigned long)hours,
             (unsigned long)minutes, (unsigned long)seconds);
}
drawStatusScreen() · source line 1419
static void drawStatusScreen(uint16_t *frame)

Calls into this documentation:

  1. Executeconst DungeonPlayerState *player = getDungeonPlayerState();
  2. Executechar text[40];
  3. Executeclear(frame, COLOR_BLACK);
  4. Executedraw_text_centred(frame, 160, 5, status_page == 0U ? "STATUS" : status_page == 1U ? "INVENTORY" : "PROGRESS", 2, COLOR_CYAN);
  5. Executesnprintf(text, sizeof(text), "%u/3", status_page + 1U);
  6. Executedraw_text(frame, 292, 7, text, 1, COLOR_DARKGRAY);
  7. Decisionstatus_page == 0U
    True
      1. Executesnprintf(text, sizeof(text), "Lvl %u XP %lu/%lu", (unsigned)player->level, (unsigned long)player->experience, (unsigned long)dungeonExperienceNeeded(player->level));
      2. Executedraw_text(frame, 12, 35, text, 2, COLOR_WHITE);
      3. Executesnprintf(text, sizeof(text), "Health: %u/%u", (unsigned)player->health, (unsigned)player->maxHealth);
      4. Executedraw_text(frame, 12, 58, text, 2, COLOR_WHITE);
      5. Executesnprintf(text, sizeof(text), "Attack: %u", (unsigned)player->attack);
      6. Executedraw_text(frame, 12, 81, text, 2, COLOR_WHITE);
      7. Executesnprintf(text, sizeof(text), "Defence: %u", (unsigned)player->defence);
      8. Executedraw_text(frame, 12, 104, text, 2, COLOR_WHITE);
      9. Executesnprintf(text, sizeof(text), "Level choices: %u", (unsigned)player->unspentStatPoints);
      10. Executedraw_text(frame, 12, 127, text, 2, COLOR_WHITE);
    False
    1. Decisionstatus_page == 1U
      True
        1. Executesnprintf(text, sizeof(text), "Gold: %lu", (unsigned long)player->gold);
        2. Executedraw_text(frame, 12, 40, text, 2, COLOR_WHITE);
        3. Executesnprintf(text, sizeof(text), "Keys: %u", (unsigned)player->keys);
        4. Executedraw_text(frame, 12, 67, text, 2, COLOR_WHITE);
        5. Executesnprintf(text, sizeof(text), "Health potions: %u", (unsigned)player->healthPotions);
        6. Executedraw_text(frame, 12, 94, text, 2, COLOR_WHITE);
        7. Executesnprintf(text, sizeof(text), "Special items: %u", specialItemCount(player->inventoryFlags));
        8. Executedraw_text(frame, 12, 121, text, 2, COLOR_WHITE);
      False
        1. Executesnprintf(text, sizeof(text), "MOVES %lu", (unsigned long)player->moves);
        2. Executedraw_text_centred(frame, 160, 26, text, 2, COLOR_CYAN);
        3. ExecuteformatPlayedTime(text, sizeof(text), player->timePlayedSeconds);
        4. Executedraw_text_centred(frame, 160, 47, text, 2, COLOR_WHITE);
        5. FOR loopunsigned floor = 0; floor < DUNGEON_FLOOR_COUNT; ++floor

          Repeat body while the loop condition permits; then continue below.

            1. Executeunsigned total_rooms = 0U;
            2. Executeconst unsigned explored_rooms = exploredRoomCount(floor, &total_rooms);
            3. Executesnprintf(text, sizeof(text), "Floor %u: %u/%u rooms", floor + 1U, explored_rooms, total_rooms);
            4. Executedraw_text_centred(frame, 160, 66 + (int)floor * 22, text, 2, COLOR_WHITE);
  8. Executedraw_text_centred(frame, 160, 153, status_page == 0U && player->unspentStatPoints > 0U ? "A/K NEXT C BONUS E CLOSE" : "A/K NEXT E CLOSE BOOT BACK", 1, COLOR_LIGHTGRAY);
Read original function source
static void drawStatusScreen(uint16_t *frame)
{
    const DungeonPlayerState *player = getDungeonPlayerState();
    char text[40];
    clear(frame, COLOR_BLACK);
    draw_text_centred(frame, 160, 5, status_page == 0U ? "STATUS" : status_page == 1U ? "INVENTORY" : "PROGRESS", 2, COLOR_CYAN);
    snprintf(text, sizeof(text), "%u/3", status_page + 1U);
    draw_text(frame, 292, 7, text, 1, COLOR_DARKGRAY);
    if (status_page == 0U) {
        snprintf(text, sizeof(text), "Lvl %u  XP %lu/%lu", (unsigned)player->level, (unsigned long)player->experience, (unsigned long)dungeonExperienceNeeded(player->level)); draw_text(frame, 12, 35, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Health: %u/%u", (unsigned)player->health, (unsigned)player->maxHealth); draw_text(frame, 12, 58, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Attack: %u", (unsigned)player->attack); draw_text(frame, 12, 81, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Defence: %u", (unsigned)player->defence); draw_text(frame, 12, 104, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Level choices: %u", (unsigned)player->unspentStatPoints); draw_text(frame, 12, 127, text, 2, COLOR_WHITE);
    } else if (status_page == 1U) {
        snprintf(text, sizeof(text), "Gold: %lu", (unsigned long)player->gold); draw_text(frame, 12, 40, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Keys: %u", (unsigned)player->keys); draw_text(frame, 12, 67, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Health potions: %u", (unsigned)player->healthPotions); draw_text(frame, 12, 94, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Special items: %u", specialItemCount(player->inventoryFlags)); draw_text(frame, 12, 121, text, 2, COLOR_WHITE);
    } else {
        snprintf(text, sizeof(text), "MOVES %lu", (unsigned long)player->moves);
        draw_text_centred(frame, 160, 26, text, 2, COLOR_CYAN);
        formatPlayedTime(text, sizeof(text), player->timePlayedSeconds);
        draw_text_centred(frame, 160, 47, text, 2, COLOR_WHITE);
        for (unsigned floor = 0; floor < DUNGEON_FLOOR_COUNT; ++floor) {
            unsigned total_rooms = 0U;
            const unsigned explored_rooms = exploredRoomCount(floor, &total_rooms);
            snprintf(text, sizeof(text), "Floor %u: %u/%u rooms", floor + 1U, explored_rooms, total_rooms);
            draw_text_centred(frame, 160, 66 + (int)floor * 22, text, 2, COLOR_WHITE);
        }
    }
    draw_text_centred(frame, 160, 153, status_page == 0U && player->unspentStatPoints > 0U ? "A/K NEXT  C BONUS  E CLOSE" : "A/K NEXT  E CLOSE  BOOT BACK", 1, COLOR_LIGHTGRAY);
}
drawRewardScreen() · source line 1453
static void drawRewardScreen(uint16_t *frame)

Calls into this documentation:

  1. Decision!image_visible
    True
    1. Executeclear(frame, COLOR_BLACK);
    False

    Continue after this decision.

  2. ExecutedrawPet2001TextCentredTransparent(frame, 160, 31, reward_is_key ? "KEY" : "TREASURE", reward_is_key ? COLOR_CYAN : COLOR_YELLOW, 3);
  3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 80, reward_text, COLOR_WHITE, 2);
  4. ExecutedrawPet2001TextCentredTransparent(frame, 160, 145, "A CONTINUE", COLOR_LIGHTGRAY, 1);
Read original function source
static void drawRewardScreen(uint16_t *frame)
{
    if (!image_visible) clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 31, reward_is_key ? "KEY" : "TREASURE",
                                       reward_is_key ? COLOR_CYAN : COLOR_YELLOW, 3);
    drawPet2001TextCentredTransparent(frame, 160, 80, reward_text, COLOR_WHITE, 2);
    drawPet2001TextCentredTransparent(frame, 160, 145, "A CONTINUE", COLOR_LIGHTGRAY, 1);
}
drawTrapResultScreen() · source line 1462
static void drawTrapResultScreen(uint16_t *frame)

Calls into this documentation:

  1. Executechar text[40];
  2. Decision!image_visible
    True
    1. Executeclear(frame, COLOR_BLACK);
    False

    Continue after this decision.

  3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 8, trap_defeated ? "DEFEATED" : "TRAP!", trap_defeated ? COLOR_RED : COLOR_YELLOW, 2);
  4. Decisiontrap_defeated
    True
    1. Executesnprintf(text, sizeof(text), "RETURNED WITH HALF HEALTH");
    False
    1. Executesnprintf(text, sizeof(text), "DAMAGE %u", (unsigned)trap_damage);
  5. ExecutedrawPet2001TextCentredTransparent(frame, 160, 137, text, COLOR_YELLOW, 1);
  6. ExecutedrawPet2001TextCentredTransparent(frame, 160, 153, "A CONTINUE", COLOR_WHITE, 1);
Read original function source
static void drawTrapResultScreen(uint16_t *frame)
{
    char text[40];
    if (!image_visible) clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 8, trap_defeated ? "DEFEATED" : "TRAP!",
                                       trap_defeated ? COLOR_RED : COLOR_YELLOW, 2);
    if (trap_defeated) snprintf(text, sizeof(text), "RETURNED WITH HALF HEALTH");
    else snprintf(text, sizeof(text), "DAMAGE %u", (unsigned)trap_damage);
    drawPet2001TextCentredTransparent(frame, 160, 137, text, COLOR_YELLOW, 1);
    drawPet2001TextCentredTransparent(frame, 160, 153, "A CONTINUE", COLOR_WHITE, 1);
}
drawLockedDoorScreen() · source line 1474
static void drawLockedDoorScreen(uint16_t *frame)

Calls into this documentation:

  1. Executeclear(frame, COLOR_BLACK);
  2. ExecutedrawPet2001TextCentredTransparent(frame, 160, 37, "LOCKED DOOR", COLOR_RED, 3);
  3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 85, "FIND THE KEY", COLOR_YELLOW, 2);
  4. ExecutedrawPet2001TextCentredTransparent(frame, 160, 145, "A RETURN", COLOR_LIGHTGRAY, 1);
Read original function source
static void drawLockedDoorScreen(uint16_t *frame)
{
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 37, "LOCKED DOOR", COLOR_RED, 3);
    drawPet2001TextCentredTransparent(frame, 160, 85, "FIND THE KEY", COLOR_YELLOW, 2);
    drawPet2001TextCentredTransparent(frame, 160, 145, "A RETURN", COLOR_LIGHTGRAY, 1);
}
drawLevelBonusScreen() · source line 1482
static void drawLevelBonusScreen(uint16_t *frame)

Calls into this documentation:

  1. Executestatic const char *options[] = { "HEALTH", "ATTACK", "DEFENCE" };
  2. Executeconst DungeonPlayerState *player = getDungeonPlayerState();
  3. Executechar text[40];
  4. Executeclear(frame, COLOR_BLACK);
  5. ExecutedrawPet2001TextCentredTransparent(frame, 160, 16, "LEVEL BONUS", COLOR_CYAN, 2);
  6. Executesnprintf(text, sizeof(text), "CHOICES %u", (unsigned)player->unspentStatPoints);
  7. ExecutedrawPet2001TextCentredTransparent(frame, 160, 47, text, COLOR_WHITE, 1);
  8. FOR loopunsigned index = 0; index < 3U; ++index

    Repeat body while the loop condition permits; then continue below.

    1. ExecutedrawPet2001TextCentredTransparent(frame, 160, 70 + (int)index * 18, options[index], index == selected_stat_option ? COLOR_YELLOW : COLOR_WHITE, 1);
  9. ExecutedrawPet2001TextCentredTransparent(frame, 160, 153, "UP/DOWN SELECT A CHOOSE E BACK", COLOR_LIGHTGRAY, 1);
Read original function source
static void drawLevelBonusScreen(uint16_t *frame)
{
    static const char *options[] = { "HEALTH", "ATTACK", "DEFENCE" };
    const DungeonPlayerState *player = getDungeonPlayerState();
    char text[40];
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 16, "LEVEL BONUS", COLOR_CYAN, 2);
    snprintf(text, sizeof(text), "CHOICES %u", (unsigned)player->unspentStatPoints);
    drawPet2001TextCentredTransparent(frame, 160, 47, text, COLOR_WHITE, 1);
    for (unsigned index = 0; index < 3U; ++index)
        drawPet2001TextCentredTransparent(frame, 160, 70 + (int)index * 18, options[index],
                                           index == selected_stat_option ? COLOR_YELLOW : COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 160, 153, "UP/DOWN SELECT  A CHOOSE  E BACK",
                                       COLOR_LIGHTGRAY, 1);
}
drawShopScreen() · source line 1498
static void drawShopScreen(uint16_t *frame)

Calls into this documentation:

  1. Executestatic const char *labels[] = { "RESTORE HP", "MAX HP +", "ATTACK +", "DEFENCE +", "LEAVE SHOP" };
  2. Executechar text[40];
  3. Executeclear(frame, COLOR_BLACK);
  4. ExecutedrawPet2001TextCentredTransparent(frame, 160, 8, "SHOP", COLOR_CYAN, 2);
  5. Executesnprintf(text, sizeof(text), "GOLD %lu", (unsigned long)getDungeonPlayerState()->gold);
  6. ExecutedrawPet2001TextCentredTransparent(frame, 160, 29, text, COLOR_YELLOW, 1);
  7. FOR loopunsigned index = 0; index < 5U; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decisionindex < 4U
        True
        1. Executesnprintf(text, sizeof(text), "%s %lu", labels[index], (unsigned long)shopChoiceCost(index));
        False
        1. Executesnprintf(text, sizeof(text), "%s", labels[index]);
      2. ExecutedrawPet2001TextCentredTransparent(frame, 160, 48 + (int)index * 17, text, index == selected_shop_option ? COLOR_YELLOW : COLOR_WHITE, 1);
  8. Decisionshop_text[0] != '\0'
    True
      1. ExecutedrawPet2001TextCentredTransparent(frame, 160, 137, shop_text, COLOR_CYAN, 1);
    False

    Continue after this decision.

  9. ExecutedrawPet2001TextCentredTransparent(frame, 160, 153, "UP/DOWN SELECT A BUY E EXIT", COLOR_LIGHTGRAY, 1);
Read original function source
static void drawShopScreen(uint16_t *frame)
{
    static const char *labels[] = { "RESTORE HP", "MAX HP +", "ATTACK +", "DEFENCE +", "LEAVE SHOP" };
    char text[40];
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 8, "SHOP", COLOR_CYAN, 2);
    snprintf(text, sizeof(text), "GOLD %lu", (unsigned long)getDungeonPlayerState()->gold);
    drawPet2001TextCentredTransparent(frame, 160, 29, text, COLOR_YELLOW, 1);
    for (unsigned index = 0; index < 5U; ++index) {
        if (index < 4U) snprintf(text, sizeof(text), "%s  %lu", labels[index], (unsigned long)shopChoiceCost(index));
        else snprintf(text, sizeof(text), "%s", labels[index]);
        drawPet2001TextCentredTransparent(frame, 160, 48 + (int)index * 17, text,
                                           index == selected_shop_option ? COLOR_YELLOW : COLOR_WHITE, 1);
    }
    if (shop_text[0] != '\0') {
        drawPet2001TextCentredTransparent(frame, 160, 137, shop_text, COLOR_CYAN, 1);
    }
    drawPet2001TextCentredTransparent(frame, 160, 153, "UP/DOWN SELECT  A BUY  E EXIT",
                                       COLOR_LIGHTGRAY, 1);
}
drawCombatScreen() · source line 1519
static void drawCombatScreen(uint16_t *frame)

Calls into this documentation:

  1. Executestatic const char *options[] = { "ATTACK", "RUN", "POTION" };
  2. Executeconst DungeonPlayerState *player = getDungeonPlayerState();
  3. Executechar text[40];
  4. Decision!image_visible
    True
    1. Executeclear(frame, COLOR_BLACK);
    False

    Continue after this decision.

  5. Executedraw_text_centred(frame, 160, 5, combatEnemyName(combat_state.enemyType), 2, COLOR_RED);
  6. Executedraw_text(frame, 7, 34, "YOU", 1, COLOR_CYAN);
  7. Executesnprintf(text, sizeof(text), "HP %u/%u", (unsigned)player->health, (unsigned)player->maxHealth);
  8. Executedraw_text(frame, 7, 49, text, 1, COLOR_WHITE);
  9. Executesnprintf(text, sizeof(text), "ATT %u", (unsigned)player->attack);
  10. Executedraw_text(frame, 7, 63, text, 1, COLOR_WHITE);
  11. Executesnprintf(text, sizeof(text), "DEF %u", (unsigned)player->defence);
  12. Executedraw_text(frame, 7, 77, text, 1, COLOR_WHITE);
  13. Executedraw_text(frame, 238, 34, "ENEMY", 1, COLOR_RED);
  14. Executesnprintf(text, sizeof(text), "HP %u/%u", (unsigned)combat_state.health, (unsigned)combat_state.maxHealth);
  15. Executedraw_text(frame, 238, 49, text, 1, COLOR_WHITE);
  16. Executesnprintf(text, sizeof(text), "ATT %u", (unsigned)combat_state.attack);
  17. Executedraw_text(frame, 238, 63, text, 1, COLOR_WHITE);
  18. Executesnprintf(text, sizeof(text), "DEF %u", (unsigned)combat_state.defence);
  19. Executedraw_text(frame, 238, 77, text, 1, COLOR_WHITE);
  20. Decisioncombat_text[0] != '\0'
    True
    1. Executedraw_text_centred(frame, 160, 96, combat_text, 1, COLOR_YELLOW);
    False

    Continue after this decision.

  21. FOR loopunsigned index = 0; index < combatOptionCount(); ++index

    Repeat body while the loop condition permits; then continue below.

    1. Executedraw_text_centred(frame, 160, 110 + (int)index * 14, options[index], 1, index == selected_combat_option ? COLOR_YELLOW : COLOR_WHITE);
  22. Executedraw_text_centred(frame, 160, 156, "UP/DOWN SELECT A CHOOSE", 1, COLOR_LIGHTGRAY);
Read original function source
static void drawCombatScreen(uint16_t *frame)
{
    static const char *options[] = { "ATTACK", "RUN", "POTION" };
    const DungeonPlayerState *player = getDungeonPlayerState();
    char text[40];
    if (!image_visible) clear(frame, COLOR_BLACK);
    draw_text_centred(frame, 160, 5, combatEnemyName(combat_state.enemyType), 2, COLOR_RED);
    draw_text(frame, 7, 34, "YOU", 1, COLOR_CYAN);
    snprintf(text, sizeof(text), "HP %u/%u", (unsigned)player->health, (unsigned)player->maxHealth); draw_text(frame, 7, 49, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "ATT %u", (unsigned)player->attack); draw_text(frame, 7, 63, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "DEF %u", (unsigned)player->defence); draw_text(frame, 7, 77, text, 1, COLOR_WHITE);
    draw_text(frame, 238, 34, "ENEMY", 1, COLOR_RED);
    snprintf(text, sizeof(text), "HP %u/%u", (unsigned)combat_state.health, (unsigned)combat_state.maxHealth); draw_text(frame, 238, 49, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "ATT %u", (unsigned)combat_state.attack); draw_text(frame, 238, 63, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "DEF %u", (unsigned)combat_state.defence); draw_text(frame, 238, 77, text, 1, COLOR_WHITE);
    if (combat_text[0] != '\0') draw_text_centred(frame, 160, 96, combat_text, 1, COLOR_YELLOW);
    for (unsigned index = 0; index < combatOptionCount(); ++index)
        draw_text_centred(frame, 160, 110 + (int)index * 14, options[index], 1, index == selected_combat_option ? COLOR_YELLOW : COLOR_WHITE);
    draw_text_centred(frame, 160, 156, "UP/DOWN SELECT  A CHOOSE", 1, COLOR_LIGHTGRAY);
}
drawCombatResultScreen() · source line 1540
static void drawCombatResultScreen(uint16_t *frame)

Calls into this documentation:

  1. Executeclear(frame, COLOR_BLACK);
  2. ExecutedrawPet2001TextCentredTransparent(frame, 160, 37, combat_result_title == NULL ? "COMBAT" : combat_result_title, combat_result_color, 3);
  3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 88, combat_text, COLOR_WHITE, 1);
  4. ExecutedrawPet2001TextCentredTransparent(frame, 160, 145, "A CONTINUE", COLOR_LIGHTGRAY, 1);
Read original function source
static void drawCombatResultScreen(uint16_t *frame)
{
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 37,
                                       combat_result_title == NULL ? "COMBAT" : combat_result_title,
                                       combat_result_color, 3);
    drawPet2001TextCentredTransparent(frame, 160, 88, combat_text, COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 160, 145, "A CONTINUE", COLOR_LIGHTGRAY, 1);
}
drawCompleteScreen() · source line 1550
static void drawCompleteScreen(uint16_t *frame)

Calls into this documentation:

  1. Executeconst DungeonPlayerState *player = getDungeonPlayerState();
  2. Executechar text[40];
  3. Executeclear(frame, COLOR_BLACK);
  4. ExecutedrawPet2001TextCentredTransparent(frame, 160, 18, "YOU WIN", COLOR_GREEN, 3);
  5. Executesnprintf(text, sizeof(text), "LEVEL %u XP %lu", (unsigned)player->level, (unsigned long)player->experience);
  6. ExecutedrawPet2001TextCentredTransparent(frame, 160, 67, text, COLOR_WHITE, 1);
  7. Executesnprintf(text, sizeof(text), "GOLD %lu MOVES %lu", (unsigned long)player->gold, (unsigned long)player->moves);
  8. ExecutedrawPet2001TextCentredTransparent(frame, 160, 89, text, COLOR_YELLOW, 1);
  9. Decisionhistory_result.error == DUNGEON_HISTORY_OK && completion_rank > 0U
    True
      1. Executesnprintf(text, sizeof(text), "HALL OF FAME #%u", (unsigned)completion_rank);
      2. ExecutedrawPet2001TextCentredTransparent(frame, 160, 112, text, COLOR_CYAN, 1);
    False

    Continue after this decision.

  10. ExecutedrawPet2001TextCentredTransparent(frame, 160, 145, "A RETURN TO GAMES", COLOR_LIGHTGRAY, 1);
Read original function source
static void drawCompleteScreen(uint16_t *frame)
{
    const DungeonPlayerState *player = getDungeonPlayerState();
    char text[40];
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 18, "YOU WIN", COLOR_GREEN, 3);
    snprintf(text, sizeof(text), "LEVEL %u  XP %lu", (unsigned)player->level, (unsigned long)player->experience);
    drawPet2001TextCentredTransparent(frame, 160, 67, text, COLOR_WHITE, 1);
    snprintf(text, sizeof(text), "GOLD %lu  MOVES %lu", (unsigned long)player->gold, (unsigned long)player->moves);
    drawPet2001TextCentredTransparent(frame, 160, 89, text, COLOR_YELLOW, 1);
    if (history_result.error == DUNGEON_HISTORY_OK && completion_rank > 0U) {
        snprintf(text, sizeof(text), "HALL OF FAME #%u", (unsigned)completion_rank);
        drawPet2001TextCentredTransparent(frame, 160, 112, text, COLOR_CYAN, 1);
    }
    drawPet2001TextCentredTransparent(frame, 160, 145, "A RETURN TO GAMES", COLOR_LIGHTGRAY, 1);
}
drawHistoryScreen() · source line 1567
static void drawHistoryScreen(uint16_t *frame)

Calls into this documentation:

  1. Executechar text[40];
  2. Executeclear(frame, COLOR_BLACK);
  3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 7, "DUNGEON HALL OF FAME", COLOR_CYAN, 2);
  4. Decisionhistory_result.error != DUNGEON_HISTORY_OK
    True
      1. Executeconst char *error_text = dungeonHistoryErrorText(history_result.error);
      2. Executeconst int error_scale = strlen(error_text) > 20U ? 1 : 2;
      3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 76, error_text, COLOR_YELLOW, error_scale);
    False
    1. DecisiondungeonHistoryCount() == 0U
      True
        1. ExecutedrawPet2001TextCentredTransparent(frame, 160, 76, "NO COMPLETIONS YET", COLOR_YELLOW, 2);
      False
        1. FOR loopunsigned index = 0; index < dungeonHistoryCount(); ++index

          Repeat body while the loop condition permits; then continue below.

            1. Executeconst DungeonHistoryRecord *record = dungeonHistoryRecord((uint8_t)index);
            2. Executesnprintf(text, sizeof(text), "%u %luG L%u %luM", index + 1U, (unsigned long)record->gold, (unsigned)record->level, (unsigned long)record->moves);
            3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 32 + (int)index * 21, text, index == 0U ? COLOR_YELLOW : COLOR_WHITE, 1);
  5. ExecutedrawPet2001TextCentredTransparent(frame, 160, 153, "A RETURN TO MENU BOOT BACK", COLOR_LIGHTGRAY, 1);
Read original function source
static void drawHistoryScreen(uint16_t *frame)
{
    char text[40];
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 7, "DUNGEON HALL OF FAME", COLOR_CYAN, 2);
    if (history_result.error != DUNGEON_HISTORY_OK) {
        const char *error_text = dungeonHistoryErrorText(history_result.error);
        // PET at 2x fits 20 cells; retain longer checksum diagnostics in full.
        const int error_scale = strlen(error_text) > 20U ? 1 : 2;
        drawPet2001TextCentredTransparent(frame, 160, 76, error_text, COLOR_YELLOW, error_scale);
    } else if (dungeonHistoryCount() == 0U) {
        drawPet2001TextCentredTransparent(frame, 160, 76, "NO COMPLETIONS YET", COLOR_YELLOW, 2);
    } else {
        for (unsigned index = 0; index < dungeonHistoryCount(); ++index) {
            const DungeonHistoryRecord *record = dungeonHistoryRecord((uint8_t)index);
            snprintf(text, sizeof(text), "%u  %luG  L%u  %luM", index + 1U,
                     (unsigned long)record->gold, (unsigned)record->level, (unsigned long)record->moves);
            drawPet2001TextCentredTransparent(frame, 160, 32 + (int)index * 21, text,
                                               index == 0U ? COLOR_YELLOW : COLOR_WHITE, 1);
        }
    }
    drawPet2001TextCentredTransparent(frame, 160, 153, "A RETURN TO MENU  BOOT BACK",
                                       COLOR_LIGHTGRAY, 1);
}
drawDungeon() · source line 1595
void drawDungeon(uint16_t *frame)

Calls into this documentation:

  1. Decision!running || frame == NULL
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Decisionstate == DUNGEON_COMBAT
    True
      1. ExecutedrawCombatScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  3. Decisionstate == DUNGEON_COMBAT_RESULT
    True
      1. ExecutedrawCombatResultScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  4. Decisionstate == DUNGEON_COMPLETE
    True
      1. ExecutedrawCompleteScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisionstate == DUNGEON_HISTORY
    True
      1. ExecutedrawHistoryScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  6. Decisionstate == DUNGEON_TRAP_RESULT
    True
      1. ExecutedrawTrapResultScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  7. Decisionstate == DUNGEON_LOCKED_DOOR
    True
      1. ExecutedrawLockedDoorScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  8. Decisionstate == DUNGEON_LEVEL_BONUS
    True
      1. ExecutedrawLevelBonusScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  9. Decisionstate == DUNGEON_SHOP
    True
      1. ExecutedrawShopScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  10. Decisionstate == DUNGEON_REWARD
    True
      1. ExecutedrawRewardScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  11. Decisionstate == DUNGEON_ROOM && status_visible
    True
      1. ExecutedrawStatusScreen(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  12. Decisionstate == DUNGEON_ROOM && map_visible
    True
      1. ExecutedrawDungeonMap(frame);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  13. Decisionstate == DUNGEON_ROOM && decode_pending
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  14. Decisionstate == DUNGEON_ROOM && image_visible
    True
      1. Executeconst DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
      2. Decision(cell->exits & DUNGEON_EXIT_NORTH) != 0U
        True
        1. ExecutedrawExitIcon(frame, DUNGEON_EXIT_NORTH, exitDestinationExplored(DUNGEON_EXIT_NORTH) ? COLOR_GREEN : COLOR_CYAN);
        False

        Continue after this decision.

      3. Decision(cell->exits & DUNGEON_EXIT_EAST) != 0U
        True
        1. ExecutedrawExitIcon(frame, DUNGEON_EXIT_EAST, exitDestinationExplored(DUNGEON_EXIT_EAST) ? COLOR_GREEN : COLOR_CYAN);
        False

        Continue after this decision.

      4. Decision(cell->exits & DUNGEON_EXIT_SOUTH) != 0U
        True
        1. ExecutedrawExitIcon(frame, DUNGEON_EXIT_SOUTH, exitDestinationExplored(DUNGEON_EXIT_SOUTH) ? COLOR_GREEN : COLOR_CYAN);
        False

        Continue after this decision.

      5. Decision(cell->exits & DUNGEON_EXIT_WEST) != 0U
        True
        1. ExecutedrawExitIcon(frame, DUNGEON_EXIT_WEST, exitDestinationExplored(DUNGEON_EXIT_WEST) ? COLOR_GREEN : COLOR_CYAN);
        False

        Continue after this decision.

      6. DecisionstairAvailable(true)
        True
        1. ExecutedrawStairIcon(frame, true);
        False

        Continue after this decision.

      7. DecisionstairAvailable(false)
        True
        1. ExecutedrawStairIcon(frame, false);
        False

        Continue after this decision.

      8. DecisionisShopRoom(cell)
        True
        1. ExecutedrawShopIndicator(frame);
        False

        Continue after this decision.

      9. Decisionai_mode
        True
          1. Executechar ai_text[28];
          2. Executesnprintf(ai_text, sizeof(ai_text), "AI %s E SPEED", aiSpeedShortText(ai_speed_index));
          3. Executedraw_text_centred(frame, 160, 156, ai_text, 1, COLOR_YELLOW);
        False

        Continue after this decision.

      10. Exit this functionreturn;
    False

    Continue after this decision.

  15. Decisionstate == DUNGEON_START_MENU && decode_pending
    True
      1. Executeclear(frame, COLOR_BLACK);
      2. Exit this functionreturn;
    False

    Continue after this decision.

  16. Decisionstate != DUNGEON_START_MENU || !image_visible
    True
    1. Executeclear(frame, COLOR_BLACK);
    False

    Continue after this decision.

  17. ExecutedrawPet2001TextCentredTransparent(frame, 160, 10, "DUNGEON", COLOR_CYAN, 3);
  18. Decisionstate == DUNGEON_CREATING
    True
      1. Executeconst char *checkpoint = "READY TO PLAY";
      2. Decisioncreation_step == CREATE_DELETE
        True
        1. Executecheckpoint = "CLEARING OLD FILES";
        False
        1. Decisioncreation_step == CREATE_GENERATE
          True
          1. Executecheckpoint = "MAP / ROOMS / KEYS";
          False
          1. Decisioncreation_step == CREATE_WORLD
            True
            1. Executecheckpoint = "SAVING DUNGEON MAP";
            False
            1. Decisioncreation_step == CREATE_PLAYER
              True
              1. Executecheckpoint = "PLAYER / LEVEL";
              False
              1. Decisioncreation_step == CREATE_EXPLORED
                True
                1. Executecheckpoint = "EXPLORING MAP";
                False

                Continue after this decision.

      3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 43, "CREATING NEW DUNGEON", COLOR_WHITE, 2);
      4. ExecutedrawPet2001TextCentredTransparent(frame, 160, 82, checkpoint, COLOR_CYAN, 2);
      5. ExecutedrawPet2001TextCentredTransparent(frame, 160, 120, "PLEASE WAIT", COLOR_LIGHTGRAY, 1);
    False
    1. Decisionstorage_result.error != DUNGEON_STORAGE_OK
      True
        1. ExecutedrawPet2001TextCentredTransparent(frame, 160, 62, dungeonStorageErrorText(storage_result.error), COLOR_RED, 2);
        2. ExecutedrawPet2001TextCentredTransparent(frame, 160, 88, "/spiffs/dungeon.dat", COLOR_WHITE, 1);
        3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 116, creation_trace[0] != '\0' ? creation_trace : "A ERASES + CREATES NEW", COLOR_YELLOW, 1);
      False
      1. Decisionstate == DUNGEON_AI_START
        True
          1. Executechar countdown[20];
          2. ExecutedrawPet2001TextCentredTransparent(frame, 160, 38, "AI MODE", COLOR_CYAN, 3);
          3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 80, "NEW DUNGEON", COLOR_WHITE, 2);
          4. Executesnprintf(countdown, sizeof(countdown), "STARTING IN %u", (unsigned)ai_start_seconds);
          5. ExecutedrawPet2001TextCentredTransparent(frame, 160, 124, countdown, COLOR_LIGHTGRAY, 1);
        False
        1. Decisionstate == DUNGEON_START_MENU
          True
            1. FOR loopunsigned index = 0; index < sizeof(start_options) / sizeof(start_options[0]); ++index

              Repeat body while the loop condition permits; then continue below.

                1. ExecutedrawPet2001TextCentredTransparent(frame, 160, 43 + (int)index * 21, start_options[index], index == selected_option ? COLOR_CYAN : COLOR_WHITE, index == selected_option ? 2 : 1);
          False
          1. Decisionstate == DUNGEON_NOTICE
            True
              1. Executeconst char *notice_text = player_result.error != DUNGEON_PLAYER_OK ? dungeonPlayerErrorText(player_result.error) : explored_result.error != DUNGEON_EXPLORED_OK ? dungeonExploredErrorText(explored_result.error) : selected_option == 1U ? "NEW DUNGEON NEXT" : selected_option == 2U ? "AI MODE NEXT" : "BOOT RETURNS TO GAMES";
              2. Executeconst int notice_scale = strlen(notice_text) > 20U ? 1 : 2;
              3. ExecutedrawPet2001TextCentredTransparent(frame, 160, 67, notice_text, COLOR_YELLOW, notice_scale);
              4. ExecutedrawPet2001TextCentredTransparent(frame, 160, 92, creation_trace, COLOR_WHITE, 1);
              5. ExecutedrawPet2001TextCentredTransparent(frame, 160, 116, "A RETURNS TO MENU", COLOR_WHITE, 1);
            False
              1. ExecutedrawPet2001TextCentredTransparent(frame, 160, 75, "ENTRANCE IMAGE FAILED", COLOR_RED, 1);
              2. ExecutedrawPet2001TextCentredTransparent(frame, 160, 104, tile_path, COLOR_WHITE, 1);
  19. Decisionstate != DUNGEON_CREATING
    True
      1. ExecutedrawPet2001TextCentredTransparent(frame, 160, 156, "UP/DOWN SELECT A OPEN BOOT BACK", COLOR_LIGHTGRAY, 1);
    False

    Continue after this decision.

Read original function source
void drawDungeon(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == DUNGEON_COMBAT) {
        drawCombatScreen(frame);
        return;
    }
    if (state == DUNGEON_COMBAT_RESULT) {
        drawCombatResultScreen(frame);
        return;
    }
    if (state == DUNGEON_COMPLETE) {
        drawCompleteScreen(frame);
        return;
    }
    if (state == DUNGEON_HISTORY) {
        drawHistoryScreen(frame);
        return;
    }
    if (state == DUNGEON_TRAP_RESULT) {
        drawTrapResultScreen(frame);
        return;
    }
    if (state == DUNGEON_LOCKED_DOOR) {
        drawLockedDoorScreen(frame);
        return;
    }
    if (state == DUNGEON_LEVEL_BONUS) {
        drawLevelBonusScreen(frame);
        return;
    }
    if (state == DUNGEON_SHOP) {
        drawShopScreen(frame);
        return;
    }
    if (state == DUNGEON_REWARD) {
        drawRewardScreen(frame);
        return;
    }
    if (state == DUNGEON_ROOM && status_visible) {
        drawStatusScreen(frame);
        return;
    }
    if (state == DUNGEON_ROOM && map_visible) {
        drawDungeonMap(frame);
        return;
    }
    if (state == DUNGEON_ROOM && decode_pending) return;
    if (state == DUNGEON_ROOM && image_visible) {
        const DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
        if ((cell->exits & DUNGEON_EXIT_NORTH) != 0U) drawExitIcon(frame, DUNGEON_EXIT_NORTH, exitDestinationExplored(DUNGEON_EXIT_NORTH) ? COLOR_GREEN : COLOR_CYAN);
        if ((cell->exits & DUNGEON_EXIT_EAST) != 0U) drawExitIcon(frame, DUNGEON_EXIT_EAST, exitDestinationExplored(DUNGEON_EXIT_EAST) ? COLOR_GREEN : COLOR_CYAN);
        if ((cell->exits & DUNGEON_EXIT_SOUTH) != 0U) drawExitIcon(frame, DUNGEON_EXIT_SOUTH, exitDestinationExplored(DUNGEON_EXIT_SOUTH) ? COLOR_GREEN : COLOR_CYAN);
        if ((cell->exits & DUNGEON_EXIT_WEST) != 0U) drawExitIcon(frame, DUNGEON_EXIT_WEST, exitDestinationExplored(DUNGEON_EXIT_WEST) ? COLOR_GREEN : COLOR_CYAN);
        if (stairAvailable(true)) drawStairIcon(frame, true);
        if (stairAvailable(false)) drawStairIcon(frame, false);
        if (isShopRoom(cell)) drawShopIndicator(frame);
        if (ai_mode) {
            char ai_text[28];
            snprintf(ai_text, sizeof(ai_text), "AI %s  E SPEED", aiSpeedShortText(ai_speed_index));
            draw_text_centred(frame, 160, 156, ai_text, 1, COLOR_YELLOW);
        }
        return;
    }
    if (state == DUNGEON_START_MENU && decode_pending) {
        // Avoid showing the previous screen during the one-frame JPEG decode.
        clear(frame, COLOR_BLACK);
        return;
    }
    // PET start-menu text is transparent, so retain the decoded JPEG beneath it.
    if (state != DUNGEON_START_MENU || !image_visible) clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 10, "DUNGEON", COLOR_CYAN, 3);
    if (state == DUNGEON_CREATING) {
        const char *checkpoint = "READY TO PLAY";
        if (creation_step == CREATE_DELETE) checkpoint = "CLEARING OLD FILES";
        else if (creation_step == CREATE_GENERATE) checkpoint = "MAP / ROOMS / KEYS";
        else if (creation_step == CREATE_WORLD) checkpoint = "SAVING DUNGEON MAP";
        else if (creation_step == CREATE_PLAYER) checkpoint = "PLAYER / LEVEL";
        else if (creation_step == CREATE_EXPLORED) checkpoint = "EXPLORING MAP";
        drawPet2001TextCentredTransparent(frame, 160, 43, "CREATING NEW DUNGEON", COLOR_WHITE, 2);
        drawPet2001TextCentredTransparent(frame, 160, 82, checkpoint, COLOR_CYAN, 2);
        drawPet2001TextCentredTransparent(frame, 160, 120, "PLEASE WAIT", COLOR_LIGHTGRAY, 1);
    } else if (storage_result.error != DUNGEON_STORAGE_OK) {
        drawPet2001TextCentredTransparent(frame, 160, 62, dungeonStorageErrorText(storage_result.error), COLOR_RED, 2);
        drawPet2001TextCentredTransparent(frame, 160, 88, "/spiffs/dungeon.dat", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 116, creation_trace[0] != '\0' ? creation_trace : "A ERASES + CREATES NEW", COLOR_YELLOW, 1);
    } else if (state == DUNGEON_AI_START) {
        char countdown[20];
        drawPet2001TextCentredTransparent(frame, 160, 38, "AI MODE", COLOR_CYAN, 3);
        drawPet2001TextCentredTransparent(frame, 160, 80, "NEW DUNGEON", COLOR_WHITE, 2);
        snprintf(countdown, sizeof(countdown), "STARTING IN %u", (unsigned)ai_start_seconds);
        drawPet2001TextCentredTransparent(frame, 160, 124, countdown, COLOR_LIGHTGRAY, 1);
    } else if (state == DUNGEON_START_MENU) {
        for (unsigned index = 0; index < sizeof(start_options) / sizeof(start_options[0]); ++index) {
            drawPet2001TextCentredTransparent(frame, 160, 43 + (int)index * 21, start_options[index],
                                               index == selected_option ? COLOR_CYAN : COLOR_WHITE,
                                               index == selected_option ? 2 : 1);
        }
    } else if (state == DUNGEON_NOTICE) {
        const char *notice_text = player_result.error != DUNGEON_PLAYER_OK ? dungeonPlayerErrorText(player_result.error) : explored_result.error != DUNGEON_EXPLORED_OK ? dungeonExploredErrorText(explored_result.error) : selected_option == 1U ? "NEW DUNGEON NEXT" : selected_option == 2U ? "AI MODE NEXT" : "BOOT RETURNS TO GAMES";
        // PET at 2x fits 20 cells. Keep longer storage/player diagnostics visible.
        const int notice_scale = strlen(notice_text) > 20U ? 1 : 2;
        drawPet2001TextCentredTransparent(frame, 160, 67, notice_text, COLOR_YELLOW, notice_scale);
        drawPet2001TextCentredTransparent(frame, 160, 92, creation_trace, COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 116, "A RETURNS TO MENU", COLOR_WHITE, 1);
    } else {
        drawPet2001TextCentredTransparent(frame, 160, 75, "ENTRANCE IMAGE FAILED", COLOR_RED, 1);
        drawPet2001TextCentredTransparent(frame, 160, 104, tile_path, COLOR_WHITE, 1);
    }
    if (state != DUNGEON_CREATING) {
        drawPet2001TextCentredTransparent(frame, 160, 156, "UP/DOWN SELECT  A OPEN  BOOT BACK",
                                           COLOR_LIGHTGRAY, 1);
    }
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "dungeon.h"
#include "dungeon_combat.h"
#include "dungeon_config.h"
#include "dungeon_storage.h"
#include "dungeon_tiles.h"
#include "dungeon_player.h"
#include "dungeon_explored.h"
#include "dungeon_history.h"
#include "dungeon_generator.h"
#include "ai_speed.h"
#include "game_graphics.h"
#include "PET2001_font.h"
#include "image_decoder.h"
#include "menu.h"
#include "esp_random.h"
#include "esp_log.h"
#include "esp_system.h"
#include <stdio.h>
#include <string.h>

#define JOYSTICK_ENTER 600
#define JOYSTICK_RELEASE 350
#define REPEAT_START_MS 400U
#define REPEAT_MS 150U
#define DUNGEON_CREATION_STAGE_PRESENT_MS 250U
#define DUNGEON_CREATION_READY_MS 500U
#define DUNGEON_TILE_ROOT "/spiffs/dungeon/dungeon_tiles"
#define DUNGEON_START_BACKGROUND_PATH DUNGEON_TILE_ROOT "/dungeon_back.jpg"
#define DUNGEON_RUN_KEY_REVEALED 0x00000001U
#define DUNGEON_RUN_COMPLETED    0x00000002U

/* -------------------------------------------------------------------------- */
/* Dungeon session state                                                      */
/* -------------------------------------------------------------------------- */
typedef enum { DUNGEON_START_MENU, DUNGEON_AI_START, DUNGEON_CREATING, DUNGEON_ROOM, DUNGEON_NOTICE, DUNGEON_REWARD, DUNGEON_TRAP_RESULT, DUNGEON_LOCKED_DOOR, DUNGEON_LEVEL_BONUS, DUNGEON_SHOP, DUNGEON_COMBAT, DUNGEON_COMBAT_RESULT, DUNGEON_COMPLETE, DUNGEON_HISTORY } dungeon_state_t;
typedef enum { COMBAT_ATTACK, COMBAT_RUN, COMBAT_POTION } combat_option_t;
typedef enum { CREATE_DELETE, CREATE_GENERATE, CREATE_WORLD, CREATE_PLAYER, CREATE_EXPLORED, CREATE_READY } creation_step_t;
static const char *start_options[] = { "CONTINUE", "NEW", "AI", "HIGH SCORES", "QUIT" };
static DungeonStorageResult storage_result;
static DungeonDeleteResult delete_result;
static DungeonPlayerResult player_result;
static DungeonExploredResult explored_result;
static DungeonHistoryResult history_result;
static dungeon_state_t state;
static char tile_path[112];
static char reward_text[40];
static char combat_text[40];
static char shop_text[40];
static char creation_trace[64];
static uint16_t trap_damage;
static uint16_t combat_levels_gained;
static bool reward_is_key;
static bool combat_victory;
static bool trap_defeated;
static const char *combat_result_title;
static uint16_t combat_result_color;
static uint8_t completion_rank;
static DungeonCombatState combat_state;
static DungeonPosition encounter_source;
static uint32_t last_move;
static unsigned selected_option, selected_combat_option, selected_stat_option, selected_shop_option;
static unsigned map_floor, map_page, status_page;
static DungeonPosition player_position;
static int held_direction;
static bool running, decode_pending, image_visible, waiting_for_centre, repeating, map_visible, status_visible;
static bool play_timer_running;
static uint32_t last_play_time_ms;
static bool ai_mode;
static uint32_t ai_start_at;
static uint8_t ai_start_seconds;
static uint32_t ai_last_move;
static bool ai_reverse_stair_blocked;
static bool ai_blocked_stair_up;
static bool ai_shop_visited;
static unsigned ai_last_shop_option;
static uint8_t ai_speed_index;
static uint16_t ai_route_queue[DUNGEON_FLOOR_COUNT * DUNGEON_WIDTH * DUNGEON_HEIGHT];
static uint8_t ai_route_first_option[DUNGEON_FLOOR_COUNT * DUNGEON_WIDTH * DUNGEON_HEIGHT];
static DungeonWorldGenerationStats creation_generation_stats;
static creation_step_t creation_step;
static uint32_t creation_step_at;
static uint32_t creation_ready_at;

static void traceDungeonCreation(const char *stage, int error, int errno_value)
{
    /* Temporary creation diagnostics retained for future SPIFFS investigation.
    snprintf(creation_trace, sizeof(creation_trace), "%s E%d N%d", stage, error, errno_value);
    ESP_LOGI("dungeon", "CREATE %s error=%d errno=%d", stage, error, errno_value);
    */
    (void)stage;
    (void)error;
    (void)errno_value;
}

static void startPlayTimer(uint32_t now_ms)
{
    last_play_time_ms = now_ms;
    play_timer_running = true;
}

static void commitPlayTime(uint32_t now_ms)
{
    if (!play_timer_running) return;
    const uint32_t elapsed = now_ms - last_play_time_ms;
    const uint32_t elapsed_seconds = elapsed / 1000U;
    if (elapsed_seconds == 0U) return;
    DungeonPlayerState *player = getDungeonPlayerState();
    player->timePlayedSeconds = UINT32_MAX - player->timePlayedSeconds < elapsed_seconds
        ? UINT32_MAX : player->timePlayedSeconds + elapsed_seconds;
    last_play_time_ms += elapsed_seconds * 1000U;
}

/* -------------------------------------------------------------------------- */
/* Room image and map-exploration helpers                                     */
/* -------------------------------------------------------------------------- */
static bool prepareEntranceImage(void)
{
    DungeonWorld *world = getDungeonWorld();
    const DungeonCell *cell = &world->floors[world->entrance.floor].cells[world->entrance.y][world->entrance.x];
    return resolveDungeonTilePath(cell, tile_path, sizeof(tile_path));
}

static void prepareStartBackground(void)
{
    const int written = snprintf(tile_path, sizeof(tile_path), "%s", DUNGEON_START_BACKGROUND_PATH);
    image_visible = false;
    decode_pending = written > 0 && (size_t)written < sizeof(tile_path);
}

static bool prepareRoomImage(const DungeonPosition *position)
{
    const DungeonCell *cell = &getDungeonWorld()->floors[position->floor].cells[position->y][position->x];
    return resolveDungeonTilePath(cell, tile_path, sizeof(tile_path));
}

static void recordExplored(const DungeonPosition *position)
{
    markDungeonExplored(position, &explored_result);
}

static bool isEncounterRoom(const DungeonCell *cell)
{
    return cell->tileType == DUNGEON_TILE_ROOM_OPEN || cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_ROUND ||
           cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_SQUARE || cell->tileType == DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT ||
           cell->tileType == DUNGEON_TILE_EXIT;
}

static bool isShopRoom(const DungeonCell *cell)
{
    return cell->tileType == DUNGEON_TILE_STAIRS_UP || cell->tileType == DUNGEON_TILE_STAIRS_DOWN || cell->tileType == DUNGEON_TILE_STAIRS_BOTH;
}

static void applyMoveRecovery(DungeonPlayerState *player)
{
    const DungeonConfig *config = getDungeonConfig();
    if (config->healthRegenMoves == 0U || player->moves % config->healthRegenMoves != 0U) return;
    const uint16_t limit = (uint16_t)(((uint32_t)player->maxHealth * config->healthRegenLimitPercent + 99U) / 100U);
    if (player->health < limit) {
        const uint32_t recovered = (uint32_t)player->health + config->healthRegenAmount;
        player->health = recovered > limit ? limit : (uint16_t)recovered;
    }
}

static uint8_t encounterEnemyType(const DungeonCell *cell)
{
    const uint8_t saved_type = (uint8_t)((cell->eventFlags & DUNGEON_ENEMY_DETAIL_MASK) >> DUNGEON_ENEMY_DETAIL_SHIFT);
    if (saved_type != DUNGEON_ENEMY_NONE) return saved_type;
    const DungeonConfig *config = getDungeonConfig();
    return player_position.floor == 0U ? config->floor1EnemyMinLevel : player_position.floor == 1U ? config->floor2EnemyMinLevel : config->floor3EnemyMinLevel;
}

static bool collectRoomReward(void);
static void returnToRoom(void);

static void prepareEventImage(const char *prefix, uint8_t variant)
{
    const unsigned image_variant = variant == 0U ? 1U : variant;
    const int written = snprintf(tile_path, sizeof(tile_path), DUNGEON_TILE_ROOT "/t_m/%s%02u.jpg", prefix, image_variant);
    image_visible = false;
    decode_pending = written > 0 && (size_t)written < sizeof(tile_path);
}

static void prepareRewardImage(const DungeonCell *cell, DungeonTreasureType treasure)
{
    const char *prefix = reward_is_key ? "t_key_" : treasure == DUNGEON_TREASURE_HEALTH ? "t_health_" :
                         treasure == DUNGEON_TREASURE_LIFE ? "t_life_" : treasure == DUNGEON_TREASURE_ATTACK ? "t_att_" :
                         treasure == DUNGEON_TREASURE_DEFENCE ? "t_def_" : "t_gold_";
    prepareEventImage(prefix, cell->variant);
}

/* -------------------------------------------------------------------------- */
/* Room events: traps, encounters, keys, and treasure                         */
/* -------------------------------------------------------------------------- */
static bool resolveRoomTrap(void)
{
    DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    const uint16_t previous_flags = cell->stateFlags;
    const DungeonConfig *config = getDungeonConfig();
    uint16_t minimum = player_position.floor == 0U ? config->floor1TrapDamageMin : player_position.floor == 1U ? config->floor2TrapDamageMin : config->floor3TrapDamageMin;
    uint16_t maximum = player_position.floor == 0U ? config->floor1TrapDamageMax : player_position.floor == 1U ? config->floor2TrapDamageMax : config->floor3TrapDamageMax;
    const uint32_t rolled_damage = minimum + esp_random() % ((uint32_t)maximum - minimum + 1U);
    const uint32_t reduced_damage = rolled_damage > player->defence ? rolled_damage - player->defence : config->combatMinimumDamage;
    trap_damage = (uint16_t)(reduced_damage < config->combatMinimumDamage ? config->combatMinimumDamage : reduced_damage);
    player->health = trap_damage >= player->health ? 0U : (uint16_t)(player->health - trap_damage);
    cell->stateFlags |= DUNGEON_STATE_TRIGGERED;
    if (player->health == 0U) {
        DungeonPosition respawn = getDungeonWorld()->entrance;
        if (player_position.floor > 0U) respawn = getDungeonWorld()->stairLinks[player_position.floor - 1U].lower;
        player->position = respawn;
        player->health = (uint16_t)(player->maxHealth / 2U + player->maxHealth % 2U);
        player_position = respawn;
        trap_defeated = true;
    } else trap_defeated = false;
    if (!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        player_position = previous_player.position;
        cell->stateFlags = previous_flags;
        saveDungeonWorld(&storage_result);
        return false;
    }
    recordExplored(&player_position);
    state = DUNGEON_TRAP_RESULT;
    prepareEventImage("t_trap_", cell->variant);
    return true;
}

static bool startRoomEvent(const DungeonPosition *source)
{
    const DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    if ((cell->eventFlags & DUNGEON_EVENT_TRAP) != 0U && (cell->stateFlags & DUNGEON_STATE_TRIGGERED) == 0U) return resolveRoomTrap();
    if ((cell->eventFlags & DUNGEON_EVENT_REQUIRES_KEY) != 0U && (cell->stateFlags & DUNGEON_STATE_UNLOCKED) == 0U && getDungeonPlayerState()->keys == 0U) {
        if (encounterEnemyType(cell) == DUNGEON_ENEMY_DRAGON) {
            DungeonPlayerState *player = getDungeonPlayerState();
            const uint32_t previous_flags = player->runFlags;
            player->runFlags |= DUNGEON_RUN_KEY_REVEALED;
            if (!saveDungeonPlayer(&player_result)) {
                player->runFlags = previous_flags;
                state = DUNGEON_NOTICE;
                return true;
            }
        }
        encounter_source = *source;
        state = DUNGEON_LOCKED_DOOR;
        image_visible = false;
        decode_pending = false;
        return true;
    }
    if (isEncounterRoom(cell) && (cell->eventFlags & DUNGEON_EVENT_ENCOUNTER) != 0U && (cell->stateFlags & DUNGEON_STATE_COMPLETED) == 0U) {
        const uint8_t enemy_type = encounterEnemyType(cell);
        if (!startDungeonCombat(enemy_type, &combat_state)) return false;
        encounter_source = *source;
        selected_combat_option = COMBAT_ATTACK;
        combat_text[0] = '\0';
        combat_victory = false;
        combat_result_title = NULL;
        state = DUNGEON_COMBAT;
        prepareEventImage(combat_state.enemyType == DUNGEON_ENEMY_LEVEL_1 ? "e_lvl1_" : combat_state.enemyType == DUNGEON_ENEMY_LEVEL_2 ? "e_lvl2_" :
                          combat_state.enemyType == DUNGEON_ENEMY_LEVEL_3 ? "e_lvl3_" : combat_state.enemyType == DUNGEON_ENEMY_LEVEL_4 ? "e_lvl4_" : "e_drag_", cell->variant);
        return true;
    }
    return collectRoomReward();
}

static bool collectRoomReward(void)
{
    DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    const uint16_t previous_flags = cell->stateFlags;
    DungeonTreasureType treasure = DUNGEON_TREASURE_GOLD;
    bool pending = false;
    bool collected = false;

    reward_text[0] = '\0';
    reward_is_key = false;
    if ((cell->eventFlags & DUNGEON_EVENT_KEY) != 0U && (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) == 0U) {
        if (player->keys < UINT16_MAX) ++player->keys;
        cell->stateFlags |= DUNGEON_STATE_KEY_COLLECTED;
        snprintf(reward_text, sizeof(reward_text), "KEY COLLECTED");
        reward_is_key = true;
        pending = true;
        collected = true;
    } else if ((cell->eventFlags & DUNGEON_EVENT_TREASURE) != 0U && (cell->stateFlags & DUNGEON_STATE_OPENED) == 0U) {
        const DungeonConfig *config = getDungeonConfig();
        treasure = (DungeonTreasureType)((cell->eventFlags & DUNGEON_TREASURE_DETAIL_MASK) >> DUNGEON_TREASURE_DETAIL_SHIFT);
        pending = true;
        if (treasure == DUNGEON_TREASURE_HEALTH) {
            if (player->health < player->maxHealth) {
                player->health = player->maxHealth;
                snprintf(reward_text, sizeof(reward_text), "HEALTH RESTORED");
                collected = true;
            } else if (player->healthPotions < config->maxHealthPotions) {
                ++player->healthPotions;
                snprintf(reward_text, sizeof(reward_text), "POTION +1");
                collected = true;
            } else snprintf(reward_text, sizeof(reward_text), "CANNOT CARRY");
        } else if (treasure == DUNGEON_TREASURE_LIFE) {
            player->maxHealth = player->maxHealth <= UINT16_MAX - config->rareLifeHealthGain ? player->maxHealth + config->rareLifeHealthGain : UINT16_MAX;
            snprintf(reward_text, sizeof(reward_text), "MAX HEALTH +%u", (unsigned)config->rareLifeHealthGain);
            collected = true;
        } else if (treasure == DUNGEON_TREASURE_ATTACK) {
            player->attack = player->attack <= UINT16_MAX - config->rareAttackGain ? player->attack + config->rareAttackGain : UINT16_MAX;
            snprintf(reward_text, sizeof(reward_text), "ATTACK +%u", (unsigned)config->rareAttackGain);
            collected = true;
        } else if (treasure == DUNGEON_TREASURE_DEFENCE) {
            player->defence = player->defence <= UINT16_MAX - config->rareDefenceGain ? player->defence + config->rareDefenceGain : UINT16_MAX;
            snprintf(reward_text, sizeof(reward_text), "DEFENCE +%u", (unsigned)config->rareDefenceGain);
            collected = true;
        } else {
            uint16_t minimum = player_position.floor == 0U ? config->floor1GoldMin : player_position.floor == 1U ? config->floor2GoldMin : config->floor3GoldMin;
            uint16_t maximum = player_position.floor == 0U ? config->floor1GoldMax : player_position.floor == 1U ? config->floor2GoldMax : config->floor3GoldMax;
            const uint32_t amount = minimum + (esp_random() % ((uint32_t)maximum - minimum + 1U));
            player->gold = player->gold <= UINT32_MAX - amount ? player->gold + amount : UINT32_MAX;
            snprintf(reward_text, sizeof(reward_text), "GOLD +%lu", (unsigned long)amount);
            collected = true;
        }
        if (collected) cell->stateFlags |= DUNGEON_STATE_OPENED;
    }

    if (!pending) return false;
    if (!collected) {
        state = DUNGEON_REWARD;
        prepareRewardImage(cell, treasure);
        return true;
    }
    if (!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        cell->stateFlags = previous_flags;
        saveDungeonWorld(&storage_result);
        return false;
    }
    state = DUNGEON_REWARD;
    prepareRewardImage(cell, treasure);
    return true;
}

static bool movePlayer(uint8_t exit)
{
    const DungeonCell *current = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    if ((current->exits & exit) == 0U) return false;
    DungeonPosition destination = player_position;
    if (exit == DUNGEON_EXIT_NORTH && destination.y > 0U) --destination.y;
    else if (exit == DUNGEON_EXIT_EAST && destination.x + 1U < DUNGEON_WIDTH) ++destination.x;
    else if (exit == DUNGEON_EXIT_SOUTH && destination.y + 1U < DUNGEON_HEIGHT) ++destination.y;
    else if (exit == DUNGEON_EXIT_WEST && destination.x > 0U) --destination.x;
    else return false;
    const DungeonCell *cell = &getDungeonWorld()->floors[destination.floor].cells[destination.y][destination.x];
    if (!resolveDungeonTilePath(cell, tile_path, sizeof(tile_path))) return false;
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    player->position = destination;
    ++player->moves;
    applyMoveRecovery(player);
    if (!saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        return false;
    }
    player_position = destination;
    recordExplored(&player_position);
    if (startRoomEvent(&previous_player.position)) return true;
    image_visible = false;
    decode_pending = true;
    return true;
}

/* -------------------------------------------------------------------------- */
/* Player movement and stair traversal                                        */
/* -------------------------------------------------------------------------- */
static bool moveStairs(bool up)
{
    DungeonWorld *world = getDungeonWorld();
    const DungeonCell *current = &world->floors[player_position.floor].cells[player_position.y][player_position.x];
    if ((up && current->tileType != DUNGEON_TILE_STAIRS_UP && current->tileType != DUNGEON_TILE_STAIRS_BOTH) ||
        (!up && current->tileType != DUNGEON_TILE_STAIRS_DOWN && current->tileType != DUNGEON_TILE_STAIRS_BOTH)) return false;
    DungeonPosition destination;
    bool found = false;

    for (unsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
        const DungeonStairLink *link = &world->stairLinks[index];
        if (player_position.floor == link->upper.floor &&
            player_position.x == link->upper.x && player_position.y == link->upper.y) {
            destination = link->lower;
            found = true;
            break;
        }
        if (player_position.floor == link->lower.floor &&
            player_position.x == link->lower.x && player_position.y == link->lower.y) {
            destination = link->upper;
            found = true;
            break;
        }
    }

    if (!found) return false;
    const DungeonCell *cell = &world->floors[destination.floor].cells[destination.y][destination.x];
    if (!resolveDungeonTilePath(cell, tile_path, sizeof(tile_path))) return false;
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    player->position = destination;
    ++player->moves;
    applyMoveRecovery(player);
    if (!saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        return false;
    }
    player_position = destination;
    recordExplored(&player_position);
    if (startRoomEvent(&previous_player.position)) return true;
    image_visible = false;
    decode_pending = true;
    return true;
}

static bool stairAvailable(bool up)
{
    DungeonWorld *world = getDungeonWorld();
    const DungeonCell *current = &world->floors[player_position.floor].cells[player_position.y][player_position.x];
    if ((up && current->tileType != DUNGEON_TILE_STAIRS_UP && current->tileType != DUNGEON_TILE_STAIRS_BOTH) ||
        (!up && current->tileType != DUNGEON_TILE_STAIRS_DOWN && current->tileType != DUNGEON_TILE_STAIRS_BOTH)) return false;
    for (unsigned index = 0; index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
        const DungeonStairLink *link = &world->stairLinks[index];
        if (player_position.floor == link->upper.floor &&
            player_position.x == link->upper.x && player_position.y == link->upper.y) return true;
        if (player_position.floor == link->lower.floor &&
            player_position.x == link->lower.x && player_position.y == link->lower.y) return true;
    }
    return false;
}

typedef enum { AI_NORTH, AI_EAST, AI_SOUTH, AI_WEST, AI_UP, AI_DOWN } ai_option_t;

static uint16_t aiPositionIndex(DungeonPosition position)
{
    return (uint16_t)(((uint16_t)position.floor * DUNGEON_WIDTH * DUNGEON_HEIGHT) +
                      ((uint16_t)position.y * DUNGEON_WIDTH) + position.x);
}

static bool aiSamePosition(DungeonPosition left, DungeonPosition right)
{
    return left.floor == right.floor && left.x == right.x && left.y == right.y;
}

static bool aiNeighbour(DungeonPosition position, ai_option_t option, DungeonPosition *destination)
{
    DungeonWorld *world = getDungeonWorld();
    *destination = position;
    if (option == AI_UP || option == AI_DOWN) {
        for (uint8_t index = 0U; index < DUNGEON_FLOOR_COUNT - 1U; ++index) {
            const DungeonStairLink *link = &world->stairLinks[index];
            if (option == AI_DOWN && aiSamePosition(position, link->upper)) { *destination = link->lower; return true; }
            if (option == AI_UP && aiSamePosition(position, link->lower)) { *destination = link->upper; return true; }
        }
        return false;
    }
    const uint8_t exits[] = { DUNGEON_EXIT_NORTH, DUNGEON_EXIT_EAST, DUNGEON_EXIT_SOUTH, DUNGEON_EXIT_WEST };
    const DungeonCell *cell = &world->floors[position.floor].cells[position.y][position.x];
    if ((cell->exits & exits[option]) == 0U) return false;
    if (option == AI_NORTH && position.y > 0U) --destination->y;
    else if (option == AI_EAST && position.x + 1U < DUNGEON_WIDTH) ++destination->x;
    else if (option == AI_SOUTH && position.y + 1U < DUNGEON_HEIGHT) ++destination->y;
    else if (option == AI_WEST && position.x > 0U) --destination->x;
    else return false;
    return true;
}

static bool aiFindPath(DungeonPosition target, ai_option_t *first_option)
{
    const DungeonPosition start = player_position;
    if (aiSamePosition(start, target)) return false;
    memset(ai_route_first_option, 0xFF, sizeof(ai_route_first_option));
    uint16_t head = 0U, tail = 0U;
    const uint16_t start_index = aiPositionIndex(start);
    ai_route_queue[tail++] = start_index;
    ai_route_first_option[start_index] = 0xFEU;
    while (head < tail) {
        const uint16_t current_index = ai_route_queue[head++];
        DungeonPosition current = { (uint8_t)(current_index / (DUNGEON_WIDTH * DUNGEON_HEIGHT)),
                                    (uint8_t)(current_index % DUNGEON_WIDTH),
                                    (uint8_t)((current_index % (DUNGEON_WIDTH * DUNGEON_HEIGHT)) / DUNGEON_WIDTH) };
        for (uint8_t option = AI_NORTH; option <= AI_DOWN; ++option) {
            DungeonPosition next;
            if (!aiNeighbour(current, (ai_option_t)option, &next)) continue;
            const uint16_t next_index = aiPositionIndex(next);
            if (ai_route_first_option[next_index] != 0xFFU) continue;
            ai_route_first_option[next_index] = current_index == start_index ? option : ai_route_first_option[current_index];
            if (aiSamePosition(next, target)) { *first_option = (ai_option_t)ai_route_first_option[next_index]; return true; }
            ai_route_queue[tail++] = next_index;
        }
    }
    return false;
}

static bool aiNearestUnexplored(ai_option_t *first_option)
{
    const DungeonPosition start = player_position;
    memset(ai_route_first_option, 0xFF, sizeof(ai_route_first_option));
    uint16_t head = 0U, tail = 0U;
    const uint16_t start_index = aiPositionIndex(start);
    ai_route_queue[tail++] = start_index;
    ai_route_first_option[start_index] = 0xFEU;
    while (head < tail) {
        const uint16_t current_index = ai_route_queue[head++];
        DungeonPosition current = { (uint8_t)(current_index / (DUNGEON_WIDTH * DUNGEON_HEIGHT)),
                                    (uint8_t)(current_index % DUNGEON_WIDTH),
                                    (uint8_t)((current_index % (DUNGEON_WIDTH * DUNGEON_HEIGHT)) / DUNGEON_WIDTH) };
        for (uint8_t option = AI_NORTH; option <= AI_DOWN; ++option) {
            DungeonPosition next;
            if (!aiNeighbour(current, (ai_option_t)option, &next)) continue;
            const uint16_t next_index = aiPositionIndex(next);
            if (ai_route_first_option[next_index] != 0xFFU) continue;
            ai_route_first_option[next_index] = current_index == start_index ? option : ai_route_first_option[current_index];
            if (!isDungeonExplored(next.floor, next.x, next.y)) { *first_option = (ai_option_t)ai_route_first_option[next_index]; return true; }
            ai_route_queue[tail++] = next_index;
        }
    }
    return false;
}

static bool aiFindUncollectedKey(DungeonPosition *position)
{
    DungeonWorld *world = getDungeonWorld();
    for (uint8_t floor = 0U; floor < DUNGEON_FLOOR_COUNT; ++floor)
        for (uint8_t y = 0U; y < DUNGEON_HEIGHT; ++y)
            for (uint8_t x = 0U; x < DUNGEON_WIDTH; ++x) {
                const DungeonCell *cell = &world->floors[floor].cells[y][x];
                if ((cell->eventFlags & DUNGEON_EVENT_KEY) != 0U &&
                    (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) == 0U) {
                    *position = (DungeonPosition){floor, x, y};
                    return true;
                }
            }
    return false;
}

static bool aiChooseMove(ai_option_t *option)
{
    DungeonPlayerState *player = getDungeonPlayerState();
    DungeonWorld *world = getDungeonWorld();
    DungeonPosition target;
    if (player->keys == 0U && (player->runFlags & DUNGEON_RUN_KEY_REVEALED) != 0U &&
        aiFindUncollectedKey(&target)) return aiFindPath(target, option);
    if (player->keys > 0U && player_position.floor < DUNGEON_FLOOR_COUNT - 1U) {
        for (uint8_t index = 0U; index < DUNGEON_FLOOR_COUNT - 1U; ++index)
            if (world->stairLinks[index].upper.floor == player_position.floor)
                return aiSamePosition(player_position, world->stairLinks[index].upper) ? ((*option = AI_DOWN), true) : aiFindPath(world->stairLinks[index].upper, option);
    }
    return aiNearestUnexplored(option);
}

static void aiMoveOnce(void)
{
    ai_option_t option;
    if (!aiChooseMove(&option)) return;
    if (ai_reverse_stair_blocked && ((option == AI_UP) == ai_blocked_stair_up)) return;
    if (option == AI_UP || option == AI_DOWN) {
        if (moveStairs(option == AI_UP)) {
            ai_reverse_stair_blocked = true;
            ai_blocked_stair_up = option == AI_DOWN;
            ai_shop_visited = false;
        }
        return;
    }
    const uint8_t exits[] = { DUNGEON_EXIT_NORTH, DUNGEON_EXIT_EAST, DUNGEON_EXIT_SOUTH, DUNGEON_EXIT_WEST };
    if (movePlayer(exits[option])) {
        ai_reverse_stair_blocked = false;
        ai_shop_visited = false;
    }
}

static void aiChooseCombatAction(void)
{
    const DungeonPlayerState *player = getDungeonPlayerState();
    if (player->health <= combat_state.health / 2U) selected_combat_option = COMBAT_RUN;
    else if (player->healthPotions > 0U && player->health <= player->maxHealth / 2U) selected_combat_option = COMBAT_POTION;
    else selected_combat_option = COMBAT_ATTACK;
}

/* -------------------------------------------------------------------------- */
/* Dungeon start, room restoration, and combat results                        */
/* -------------------------------------------------------------------------- */
void startDungeon(uint32_t now_ms)
{
    running = true;
    play_timer_running = false;
    last_play_time_ms = now_ms;
    state = DUNGEON_START_MENU;
    selected_option = 0;
    image_visible = false;
    decode_pending = false;
    waiting_for_centre = true;
    held_direction = 0;
    repeating = false;
    map_visible = false;
    status_visible = false;
    map_floor = 0;
    map_page = 0;
    status_page = 0;
    reward_text[0] = '\0';
    combat_text[0] = '\0';
    shop_text[0] = '\0';
    creation_trace[0] = '\0';
    trap_damage = 0U;
    combat_levels_gained = 0U;
    reward_is_key = false;
    combat_victory = false;
    ai_mode = false;
    ai_start_at = 0U;
    ai_start_seconds = 2U;
    ai_last_move = 0U;
    ai_reverse_stair_blocked = false;
    ai_shop_visited = false;
    ai_last_shop_option = 4U;
    ai_speed_index = AI_SPEED_DEFAULT_INDEX;
    trap_defeated = false;
    combat_result_title = NULL;
    initDungeonConfig();
    initDungeonExplored();
    initDungeonPlayer();
    memset(&player_result, 0, sizeof(player_result));
    player_result.error = DUNGEON_PLAYER_OK;
    memset(&explored_result, 0, sizeof(explored_result));
    explored_result.error = DUNGEON_EXPLORED_OK;
    memset(&history_result, 0, sizeof(history_result));
    history_result.error = DUNGEON_HISTORY_OK;
    completion_rank = 0U;
    if (!loadDungeonWorld(&storage_result)) return;
    if (!prepareEntranceImage()) {
        storage_result.error = DUNGEON_STORAGE_READ_FAILED;
        return;
    }
    prepareStartBackground();
}

void stopDungeon(uint32_t now_ms)
{
    commitPlayTime(now_ms);
    if (play_timer_running) (void)saveDungeonPlayer(&player_result);
    play_timer_running = false;
    running = false;
    decode_pending = false;
    image_visible = false;
}

static unsigned combatOptionCount(void)
{
    return getDungeonPlayerState()->healthPotions > 0U ? 3U : 2U;
}

static const char *combatEnemyName(uint8_t enemy_type)
{
    switch (enemy_type) {
        case DUNGEON_ENEMY_LEVEL_1: return "LEVEL 1";
        case DUNGEON_ENEMY_LEVEL_2: return "LEVEL 2";
        case DUNGEON_ENEMY_LEVEL_3: return "LEVEL 3";
        case DUNGEON_ENEMY_LEVEL_4: return "LEVEL 4";
        default: return "DRAGON";
    }
}

static void returnToRoom(void)
{
    if (!prepareRoomImage(&player_position)) {
        player_result.error = DUNGEON_PLAYER_INVALID_POSITION;
        state = DUNGEON_NOTICE;
        return;
    }
    state = DUNGEON_ROOM;
    image_visible = false;
    decode_pending = true;
}

static void beginDungeonCreation(bool delete_old_files, uint32_t now_ms)
{
    memset(&creation_generation_stats, 0, sizeof(creation_generation_stats));
    creation_step = delete_old_files ? CREATE_DELETE : CREATE_GENERATE;
    creation_step_at = now_ms;
    state = DUNGEON_CREATING;
    image_visible = false;
    decode_pending = false;
}

static void advanceDungeonCreation(uint32_t now_ms)
{
    if (now_ms - creation_step_at < DUNGEON_CREATION_STAGE_PRESENT_MS) return;
    if (creation_step == CREATE_DELETE) {
        if (!deleteDungeonDataFiles(&delete_result)) { state = DUNGEON_NOTICE; return; }
        prepareNewDungeonWorldStorage();
        creation_step = CREATE_GENERATE;
        creation_step_at = now_ms;
        return;
    }
    if (creation_step == CREATE_GENERATE) {
        if (!generateDungeonWorld(&creation_generation_stats)) { state = DUNGEON_NOTICE; return; }
        creation_step = CREATE_WORLD;
        creation_step_at = now_ms;
        return;
    }
    if (creation_step == CREATE_WORLD) {
        if (!saveDungeonWorld(&storage_result)) { state = DUNGEON_NOTICE; return; }
        creation_step = CREATE_PLAYER;
        creation_step_at = now_ms;
        return;
    }
    if (creation_step == CREATE_PLAYER) {
        if (!resetDungeonPlayer(&player_result)) { state = DUNGEON_NOTICE; return; }
        creation_step = CREATE_EXPLORED;
        creation_step_at = now_ms;
        return;
    }
    if (creation_step == CREATE_EXPLORED) {
        if (!resetDungeonExplored(&getDungeonPlayerState()->position, &explored_result)) { state = DUNGEON_NOTICE; return; }
        player_position = getDungeonPlayerState()->position;
        creation_step = CREATE_READY;
        creation_ready_at = now_ms;
        creation_step_at = now_ms;
        return;
    }
    if (now_ms - creation_ready_at < DUNGEON_CREATION_READY_MS) return;
    if (!prepareRoomImage(&player_position)) { player_result.error = DUNGEON_PLAYER_INVALID_POSITION; state = DUNGEON_NOTICE; return; }
    state = DUNGEON_ROOM;
    startPlayTimer(now_ms);
    image_visible = false;
    decode_pending = true;
}

static void completeCombatVictory(void)
{
    DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    const uint16_t previous_flags = cell->stateFlags;
    const DungeonPosition *exit = &getDungeonWorld()->exit;
    const bool final_dragon = combat_state.enemyType == DUNGEON_ENEMY_DRAGON && player_position.floor == exit->floor && player_position.x == exit->x && player_position.y == exit->y;
    combat_levels_gained = addDungeonPlayerExperience(combat_state.xpReward);
    cell->stateFlags |= DUNGEON_STATE_COMPLETED;
    if (final_dragon) {
        cell->stateFlags |= DUNGEON_STATE_MESSAGE_READ | DUNGEON_STATE_UNLOCKED;
        player->runFlags |= DUNGEON_RUN_COMPLETED;
    }
    if (!saveDungeonWorld(&storage_result) || !saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        cell->stateFlags = previous_flags;
        saveDungeonWorld(&storage_result);
        state = DUNGEON_NOTICE;
        return;
    }
    if (final_dragon) {
        recordDungeonCompletion(player, &history_result, &completion_rank);
        state = DUNGEON_COMPLETE;
        return;
    }
    if (combat_levels_gained > 0U) snprintf(combat_text, sizeof(combat_text), "+%lu XP  LEVEL +%u", (unsigned long)combat_state.xpReward, (unsigned)combat_levels_gained);
    else snprintf(combat_text, sizeof(combat_text), "+%lu XP", (unsigned long)combat_state.xpReward);
    combat_victory = true;
    combat_result_title = "VICTORY";
    combat_result_color = COLOR_GREEN;
    state = DUNGEON_COMBAT_RESULT;
}

static void respawnAfterCombatDefeat(void)
{
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    DungeonPosition respawn = getDungeonWorld()->entrance;
    if (player_position.floor > 0U) respawn = getDungeonWorld()->stairLinks[player_position.floor - 1U].lower;
    player->position = respawn;
    player->health = (uint16_t)(player->maxHealth / 2U + player->maxHealth % 2U);
    if (!saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        state = DUNGEON_NOTICE;
        return;
    }
    player_position = respawn;
    recordExplored(&player_position);
    snprintf(combat_text, sizeof(combat_text), "RETURNED WITH HALF HEALTH");
    combat_victory = false;
    combat_result_title = "DEFEATED";
    combat_result_color = COLOR_RED;
    state = DUNGEON_COMBAT_RESULT;
}

static void resolveCombatAction(void)
{
    DungeonPlayerState *player = getDungeonPlayerState();
    DungeonCombatResult result;
    if (selected_combat_option == COMBAT_RUN) {
        result = dungeonCombatAttemptRun(&combat_state, player);
        if (result == DUNGEON_COMBAT_ESCAPED) {
            player->position = encounter_source;
            if (!saveDungeonPlayer(&player_result)) { state = DUNGEON_NOTICE; return; }
            player_position = encounter_source;
            snprintf(combat_text, sizeof(combat_text), "ESCAPED");
            combat_victory = false;
            combat_result_title = "ESCAPED";
            combat_result_color = COLOR_CYAN;
            state = DUNGEON_COMBAT_RESULT;
            return;
        }
        snprintf(combat_text, sizeof(combat_text), "RUN FAILED  ENEMY %lu", (unsigned long)combat_state.lastEnemyDamage);
    } else if (selected_combat_option == COMBAT_POTION) {
        result = dungeonCombatUsePotion(&combat_state, player);
        snprintf(combat_text, sizeof(combat_text), "POTION  ENEMY %lu", (unsigned long)combat_state.lastEnemyDamage);
    } else {
        result = dungeonCombatAttack(&combat_state, player);
        snprintf(combat_text, sizeof(combat_text), "YOU %lu  ENEMY %lu", (unsigned long)combat_state.lastPlayerDamage, (unsigned long)combat_state.lastEnemyDamage);
    }
    if (result == DUNGEON_COMBAT_ENEMY_DEFEATED) { completeCombatVictory(); return; }
    if (result == DUNGEON_COMBAT_PLAYER_DEFEATED) { respawnAfterCombatDefeat(); return; }
    if (selected_combat_option >= combatOptionCount()) selected_combat_option = COMBAT_ATTACK;
    if (!saveDungeonPlayer(&player_result)) state = DUNGEON_NOTICE;
}

static uint32_t shopUpgradeCost(uint32_t base_cost)
{
    const DungeonConfig *config = getDungeonConfig();
    uint32_t cost = base_cost;
    for (uint16_t purchase = 0U; purchase < getDungeonPlayerState()->shopPurchaseCount; ++purchase) {
        const uint64_t increased = ((uint64_t)cost * (100U + config->shopCostIncreasePercent) + 99U) / 100U;
        if (increased >= UINT32_MAX) return UINT32_MAX;
        cost = (uint32_t)increased;
    }
    return cost;
}

static uint32_t shopChoiceCost(unsigned choice)
{
    const DungeonConfig *config = getDungeonConfig();
    if (choice == 0U) return config->shopRestoreCost;
    if (choice == 1U) return shopUpgradeCost(config->shopHealthBaseCost);
    if (choice == 2U) return shopUpgradeCost(config->shopAttackBaseCost);
    return choice == 3U ? shopUpgradeCost(config->shopDefenceBaseCost) : 0U;
}

static void applyShopChoice(void)
{
    DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonPlayerState previous_player = *player;
    const DungeonConfig *config = getDungeonConfig();
    if (selected_shop_option == 4U) { returnToRoom(); return; }
    const uint32_t cost = shopChoiceCost(selected_shop_option);
    if (player->gold < cost) { snprintf(shop_text, sizeof(shop_text), "NOT ENOUGH GOLD"); return; }
    if (selected_shop_option == 0U) {
        if (player->health >= player->maxHealth) { snprintf(shop_text, sizeof(shop_text), "HEALTH ALREADY FULL"); return; }
        player->health = player->maxHealth;
        snprintf(shop_text, sizeof(shop_text), "HEALTH RESTORED");
    } else if (selected_shop_option == 1U) {
        if (config->shopHealthGain == 0U || config->shopHealthGain > UINT16_MAX - player->maxHealth) { snprintf(shop_text, sizeof(shop_text), "MAX HP LIMIT"); return; }
        player->maxHealth += config->shopHealthGain;
        player->health = player->health <= UINT16_MAX - config->shopHealthGain ? player->health + config->shopHealthGain : UINT16_MAX;
        snprintf(shop_text, sizeof(shop_text), "MAX HP INCREASED");
    } else if (selected_shop_option == 2U) {
        if (config->shopAttackGain == 0U || config->shopAttackGain > UINT16_MAX - player->attack) { snprintf(shop_text, sizeof(shop_text), "ATTACK LIMIT"); return; }
        player->attack += config->shopAttackGain;
        snprintf(shop_text, sizeof(shop_text), "ATTACK INCREASED");
    } else {
        if (config->shopDefenceGain == 0U || config->shopDefenceGain > UINT16_MAX - player->defence) { snprintf(shop_text, sizeof(shop_text), "DEFENCE LIMIT"); return; }
        player->defence += config->shopDefenceGain;
        snprintf(shop_text, sizeof(shop_text), "DEFENCE INCREASED");
    }
    player->gold -= cost;
    if (player->shopPurchaseCount < UINT16_MAX) ++player->shopPurchaseCount;
    if (!saveDungeonPlayer(&player_result)) {
        *player = previous_player;
        state = DUNGEON_NOTICE;
    }
}

static unsigned aiChooseShopOption(void)
{
    const DungeonPlayerState *player = getDungeonPlayerState();
    const DungeonConfig *config = getDungeonConfig();
    if ((uint32_t)player->health * 2U < player->maxHealth &&
        player->health < player->maxHealth && player->gold >= shopChoiceCost(0U)) return 0U;
    const unsigned options[] = {1U, 2U, 3U};
    const uint8_t weights[] = {2U, 1U, 4U};
    uint8_t total = 0U;
    for (unsigned index = 0U; index < 3U; ++index) {
        const unsigned option = options[index];
        const bool valid = option == 1U ? config->shopHealthGain > 0U && config->shopHealthGain <= UINT16_MAX - player->maxHealth :
                           option == 2U ? config->shopAttackGain > 0U && config->shopAttackGain <= UINT16_MAX - player->attack :
                                          config->shopDefenceGain > 0U && config->shopDefenceGain <= UINT16_MAX - player->defence;
        if (valid && player->gold >= shopChoiceCost(option) && option != ai_last_shop_option) total += weights[index];
    }
    if (total == 0U) return 4U;
    uint8_t ticket = (uint8_t)(esp_random() % total);
    for (unsigned index = 0U; index < 3U; ++index) {
        const unsigned option = options[index];
        const bool valid = option == 1U ? config->shopHealthGain > 0U && config->shopHealthGain <= UINT16_MAX - player->maxHealth :
                           option == 2U ? config->shopAttackGain > 0U && config->shopAttackGain <= UINT16_MAX - player->attack :
                                          config->shopDefenceGain > 0U && config->shopDefenceGain <= UINT16_MAX - player->defence;
        if (!valid || player->gold < shopChoiceCost(option) || option == ai_last_shop_option) continue;
        if (ticket < weights[index]) return option;
        ticket = (uint8_t)(ticket - weights[index]);
    }
    return 4U;
}

/* -------------------------------------------------------------------------- */
/* Button actions: A/K action, C map/shop, E status                           */
/* -------------------------------------------------------------------------- */
void dungeonAction(bool action, bool map_action, bool status_action, bool context_action, uint32_t now_ms)
{
    if (!running) return;
    if (state == DUNGEON_COMPLETE && action) { menu_back(now_ms); return; }
    if (state == DUNGEON_HISTORY && action) { state = DUNGEON_START_MENU; return; }
    if (state == DUNGEON_SHOP) {
        if (status_action) { returnToRoom(); return; }
        if (action) applyShopChoice();
        return;
    }
    if (state == DUNGEON_LEVEL_BONUS) {
        if (status_action) { returnToRoom(); return; }
        if (action) {
            DungeonPlayerState *player = getDungeonPlayerState();
            const DungeonPlayerState previous_player = *player;
            if (applyDungeonPlayerStatChoice((uint8_t)selected_stat_option) && !saveDungeonPlayer(&player_result)) {
                *player = previous_player;
                state = DUNGEON_NOTICE;
            } else if (player->unspentStatPoints == 0U) returnToRoom();
        }
        return;
    }
    if (state == DUNGEON_LOCKED_DOOR && action) {
        DungeonPlayerState *player = getDungeonPlayerState();
        player->position = encounter_source;
        if (!saveDungeonPlayer(&player_result)) { state = DUNGEON_NOTICE; return; }
        player_position = encounter_source;
        returnToRoom();
        return;
    }
    if (state == DUNGEON_TRAP_RESULT && action) {
        if (!trap_defeated && startRoomEvent(&player_position)) return;
        returnToRoom();
        return;
    }
    if (state == DUNGEON_COMBAT_RESULT && action) {
        if (combat_victory && startRoomEvent(&player_position)) return;
        returnToRoom();
        return;
    }
    if (state == DUNGEON_COMBAT && action) { resolveCombatAction(); return; }
    if (state == DUNGEON_REWARD && action) {
        returnToRoom();
        return;
    }
    if (state == DUNGEON_ROOM && map_action) {
        if (map_visible) {
            map_visible = false;
            waiting_for_centre = true;
            returnToRoom();
            return;
        }
    }
    if (state == DUNGEON_ROOM && status_visible && context_action && status_page == 0U && getDungeonPlayerState()->unspentStatPoints > 0U) {
        status_visible = false;
        selected_stat_option = 0U;
        state = DUNGEON_LEVEL_BONUS;
        return;
    }
    if (state == DUNGEON_ROOM && !map_visible && !status_visible && context_action && isShopRoom(&getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x])) {
        selected_shop_option = 0U;
        shop_text[0] = '\0';
        state = DUNGEON_SHOP;
        image_visible = false;
        decode_pending = false;
        return;
    }
    if (state == DUNGEON_ROOM && status_action) {
        map_visible = false;
        status_visible = !status_visible;
        if (!status_visible) {
            image_visible = false;
            decode_pending = true;
        }
        waiting_for_centre = true;
        return;
    }
    if (state == DUNGEON_ROOM && status_visible) {
        if (action) status_page = (status_page + 1U) % 3U;
        return;
    }
    if (state == DUNGEON_ROOM && map_visible) {
        if (action) {
            if (map_page == 0U) map_page = 1U;
            else if (map_page == 1U) map_page = 3U;
            else if (map_page == 3U) map_page = 2U;
            else {
                map_page = 0U;
                map_floor = (map_floor + 1U) % DUNGEON_FLOOR_COUNT;
            }
        }
        waiting_for_centre = true;
        return;
    }
    if (state == DUNGEON_ROOM && map_action) {
        map_floor = player_position.floor;
        map_page = (player_position.y / 20U) * 2U + (player_position.x / 20U);
        map_visible = true;
        waiting_for_centre = true;
        return;
    }
    if (!action) return;
    if (state == DUNGEON_START_MENU && storage_result.error != DUNGEON_STORAGE_OK) {
        traceDungeonCreation("A RECEIVED", 0, 0);
        beginDungeonCreation(true, now_ms);
        return;
    }
    if (state == DUNGEON_START_MENU && storage_result.error == DUNGEON_STORAGE_OK) {
        if (selected_option == 0U) {
            if (!prepareDungeonPlayer(&player_result)) {
                state = DUNGEON_NOTICE;
                return;
            }
            state = DUNGEON_ROOM;
            player_position = getDungeonPlayerState()->position;
            if (!prepareRoomImage(&player_position)) {
                state = DUNGEON_NOTICE;
                player_result.error = DUNGEON_PLAYER_INVALID_POSITION;
                return;
            }
            if (!prepareDungeonExplored(&player_position, &explored_result)) {
                state = DUNGEON_NOTICE;
                return;
            }
            startPlayTimer(now_ms);
            if (startRoomEvent(&player_position)) return;
            image_visible = false;
            decode_pending = true;
        } else if (selected_option == 1U) {
            beginDungeonCreation(false, now_ms);
            return;
        } else if (selected_option == 2U) {
            ai_mode = true;
            ai_start_at = now_ms;
            ai_start_seconds = 2U;
            state = DUNGEON_AI_START;
        } else if (selected_option == 3U) {
            if (!loadDungeonHistory(&history_result) && history_result.error == DUNGEON_HISTORY_OPEN_READ_FAILED) initDungeonHistory();
            state = DUNGEON_HISTORY;
        } else if (selected_option == 4U) {
            menu_back(now_ms);
        } else {
            state = DUNGEON_NOTICE;
        }
    } else if (state == DUNGEON_NOTICE) {
        state = DUNGEON_START_MENU;
    }
}

void dungeonAIControl(bool speed_action, uint32_t now_ms)
{
    if (!running || !ai_mode || state != DUNGEON_ROOM || map_visible || status_visible) return;
    if (speed_action) {
        cycleAISpeed(&ai_speed_index);
        ai_last_move = now_ms;
    }
}

/* -------------------------------------------------------------------------- */
/* Joystick movement and menu selection                                       */
/* -------------------------------------------------------------------------- */
void updateDungeon(uint16_t *frame, int x_offset, int y_offset, bool joystick_valid, uint32_t now_ms)
{
    if (!running) return;
    commitPlayTime(now_ms);
    if (state == DUNGEON_AI_START) {
        if (!ai_mode) {
            state = DUNGEON_START_MENU;
            return;
        }
        const uint32_t elapsed = now_ms - ai_start_at;
        if (elapsed < 2000U) {
            ai_start_seconds = (uint8_t)((2000U - elapsed + 999U) / 1000U);
            return;
        }
        state = DUNGEON_START_MENU;
        selected_option = 1U;
        dungeonAction(true, false, false, false, now_ms);
        return;
    }
    if (state == DUNGEON_CREATING) {
        advanceDungeonCreation(now_ms);
        return;
    }
    if (decode_pending) {
        decode_pending = false;
        image_visible = imageDecodeFile(tile_path, frame);
    }
    if (ai_mode && state == DUNGEON_ROOM && !map_visible && !status_visible) {
        if (isShopRoom(&getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x]) && !ai_shop_visited) {
            ai_shop_visited = true;
            dungeonAction(false, false, false, true, now_ms);
            return;
        }
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            aiMoveOnce();
        }
        return;
    }
    if (ai_mode && (state == DUNGEON_REWARD || state == DUNGEON_TRAP_RESULT)) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            dungeonAction(true, false, false, false, now_ms);
        }
        return;
    }
    if (ai_mode && state == DUNGEON_COMBAT) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            aiChooseCombatAction();
            dungeonAction(true, false, false, false, now_ms);
        }
        return;
    }
    if (ai_mode && state == DUNGEON_COMBAT_RESULT) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            dungeonAction(true, false, false, false, now_ms);
        }
        return;
    }
    if (ai_mode && state == DUNGEON_LOCKED_DOOR) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            dungeonAction(true, false, false, false, now_ms);
        }
        return;
    }
    if (ai_mode && state == DUNGEON_SHOP) {
        if (now_ms - ai_last_move >= aiSpeedDelay(ai_speed_index)) {
            ai_last_move = now_ms;
            selected_shop_option = aiChooseShopOption();
            if (selected_shop_option != 4U) ai_last_shop_option = selected_shop_option;
            dungeonAction(true, false, selected_shop_option == 4U, false, now_ms);
        }
        return;
    }
    if ((state != DUNGEON_START_MENU && state != DUNGEON_ROOM && state != DUNGEON_COMBAT && state != DUNGEON_LEVEL_BONUS && state != DUNGEON_SHOP) || !joystick_valid || map_visible || status_visible) return;
    const int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
    const int y_magnitude = y_offset < 0 ? -y_offset : y_offset;
    if (x_magnitude <= JOYSTICK_RELEASE && y_magnitude <= JOYSTICK_RELEASE) {
        waiting_for_centre = false;
        held_direction = 0;
        repeating = false;
        return;
    }
    if (waiting_for_centre) return;

    if (state == DUNGEON_ROOM) {
        if (x_magnitude < JOYSTICK_ENTER && y_magnitude < JOYSTICK_ENTER) return;
        if (x_offset >= JOYSTICK_ENTER && y_magnitude >= JOYSTICK_ENTER) {
            moveStairs(y_offset > 0);
            waiting_for_centre = true;
            return;
        }
        uint8_t exit;
        if (x_magnitude >= y_magnitude) exit = x_offset > 0 ? DUNGEON_EXIT_EAST : DUNGEON_EXIT_WEST;
        else exit = y_offset > 0 ? DUNGEON_EXIT_NORTH : DUNGEON_EXIT_SOUTH;
        movePlayer(exit);
        waiting_for_centre = true;
        return;
    }

    if (y_magnitude < JOYSTICK_ENTER) return;
    const int direction = y_offset > 0 ? -1 : 1;
    if (direction == held_direction && now_ms - last_move < (repeating ? REPEAT_MS : REPEAT_START_MS)) return;
    repeating = direction == held_direction;
    held_direction = direction;
    last_move = now_ms;
    if (state == DUNGEON_COMBAT) {
        const unsigned count = combatOptionCount();
        if (direction < 0 && selected_combat_option > 0U) --selected_combat_option;
        if (direction > 0 && selected_combat_option + 1U < count) ++selected_combat_option;
        return;
    }
    if (state == DUNGEON_LEVEL_BONUS) {
        if (direction < 0 && selected_stat_option > 0U) --selected_stat_option;
        if (direction > 0 && selected_stat_option < 2U) ++selected_stat_option;
        return;
    }
    if (state == DUNGEON_SHOP) {
        if (direction < 0 && selected_shop_option > 0U) --selected_shop_option;
        if (direction > 0 && selected_shop_option < 4U) ++selected_shop_option;
        return;
    }
    const unsigned count = sizeof(start_options) / sizeof(start_options[0]);
    if (direction < 0 && selected_option > 0U) --selected_option;
    if (direction > 0 && selected_option + 1U < count) ++selected_option;
}

/* -------------------------------------------------------------------------- */
/* Room overlay icons                                                         */
/* -------------------------------------------------------------------------- */
static void drawBoldLine(uint16_t *frame, int x0, int y0, int x1, int y1, uint16_t color)
{
    draw_line(frame, x0, y0, x1, y1, color);
    draw_line(frame, x0 + 1, y0, x1 + 1, y1, color);
    draw_line(frame, x0, y0 + 1, x1, y1 + 1, color);
}

static bool exitDestinationExplored(uint8_t exit)
{
    int x = player_position.x;
    int y = player_position.y;
    if (exit == DUNGEON_EXIT_NORTH) --y;
    else if (exit == DUNGEON_EXIT_EAST) ++x;
    else if (exit == DUNGEON_EXIT_SOUTH) ++y;
    else if (exit == DUNGEON_EXIT_WEST) --x;
    return x >= 0 && x < (int)DUNGEON_WIDTH && y >= 0 && y < (int)DUNGEON_HEIGHT &&
           isDungeonExplored(player_position.floor, (uint8_t)x, (uint8_t)y);
}

static void drawExitIcon(uint16_t *frame, uint8_t exit, uint16_t color)
{
    int x = 160;
    int y = 85;
    if (exit == DUNGEON_EXIT_NORTH) y = 12;
    else if (exit == DUNGEON_EXIT_EAST) x = LCD_WIDTH - 13;
    else if (exit == DUNGEON_EXIT_SOUTH) y = 157;
    else if (exit == DUNGEON_EXIT_WEST) x = 12;

    if (exit == DUNGEON_EXIT_NORTH || exit == DUNGEON_EXIT_SOUTH) {
        const int direction = exit == DUNGEON_EXIT_NORTH ? -1 : 1;
        drawBoldLine(frame, x, y - direction * 7, x, y + direction * 7, color);
        drawBoldLine(frame, x, y + direction * 7, x - 5, y + direction * 2, color);
        drawBoldLine(frame, x, y + direction * 7, x + 5, y + direction * 2, color);
    } else {
        const int direction = exit == DUNGEON_EXIT_EAST ? 1 : -1;
        drawBoldLine(frame, x - direction * 7, y, x + direction * 7, y, color);
        drawBoldLine(frame, x + direction * 7, y, x + direction * 2, y - 5, color);
        drawBoldLine(frame, x + direction * 7, y, x + direction * 2, y + 5, color);
    }
}

static void drawStairIcon(uint16_t *frame, bool up)
{
    const int x = LCD_WIDTH - 44;
    const int y = up ? 12 : 157;
    const int direction = up ? -1 : 1;
    drawBoldLine(frame, x, y - direction * 7, x, y + direction * 7, COLOR_YELLOW);
    drawBoldLine(frame, x, y + direction * 7, x - 5, y + direction * 2, COLOR_YELLOW);
    drawBoldLine(frame, x, y + direction * 7, x + 5, y + direction * 2, COLOR_YELLOW);
}

static void drawShopIndicator(uint16_t *frame)
{
    draw_text(frame, 5, 5, "C SHOP", 1, COLOR_YELLOW);
}

static uint16_t mapCellColor(const DungeonCell *cell)
{
    if (cell->tileType == DUNGEON_TILE_STAIRS_UP || cell->tileType == DUNGEON_TILE_STAIRS_DOWN || cell->tileType == DUNGEON_TILE_STAIRS_BOTH) return COLOR_PURPLE;
    if (cell->tileType == DUNGEON_TILE_ENTRANCE) return COLOR_GREEN;
    if (cell->tileType == DUNGEON_TILE_EXIT) return COLOR_MAGENTA;
    if (cell->tileType == DUNGEON_TILE_PASSAGE || cell->tileType == DUNGEON_TILE_DEAD_END_PASSAGE) return COLOR_LIGHTGRAY;
    return COLOR_CYAN;
}

/* -------------------------------------------------------------------------- */
/* Explored-map drawing                                                       */
/* -------------------------------------------------------------------------- */
static void drawMapCell(uint16_t *frame, unsigned x, unsigned y, unsigned first_column, unsigned first_row, const DungeonCell *cell)
{
    const int screen_x = ((int)x - (int)first_column) * 8;
    const int screen_y = ((int)y - (int)first_row) * 8;
    const uint16_t color = mapCellColor(cell);
    const bool large = cell->tileType != DUNGEON_TILE_PASSAGE && cell->tileType != DUNGEON_TILE_DEAD_END_PASSAGE;
    fill_rect(frame, screen_x + (large ? 1 : 2), screen_y + (large ? 1 : 2), large ? 6 : 4, large ? 6 : 4, color);
    if ((cell->exits & DUNGEON_EXIT_NORTH) != 0U) {
        const bool known = y > 0U && isDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)(y - 1U));
        fill_rect(frame, screen_x + 3, screen_y, 2, 4, known ? color : COLOR_ORANGE);
        if (!known) fill_rect(frame, screen_x + 2, screen_y, 4, 2, COLOR_ORANGE);
    }
    if ((cell->exits & DUNGEON_EXIT_EAST) != 0U) {
        const bool known = x + 1U < DUNGEON_WIDTH && isDungeonExplored((uint8_t)map_floor, (uint8_t)(x + 1U), (uint8_t)y);
        fill_rect(frame, screen_x + 4, screen_y + 3, 4, 2, known ? color : COLOR_ORANGE);
        if (!known) fill_rect(frame, screen_x + 6, screen_y + 2, 2, 4, COLOR_ORANGE);
    }
    if ((cell->exits & DUNGEON_EXIT_SOUTH) != 0U) {
        const bool known = y + 1U < DUNGEON_HEIGHT && isDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)(y + 1U));
        fill_rect(frame, screen_x + 3, screen_y + 4, 2, 4, known ? color : COLOR_ORANGE);
        if (!known) fill_rect(frame, screen_x + 2, screen_y + 6, 4, 2, COLOR_ORANGE);
    }
    if ((cell->exits & DUNGEON_EXIT_WEST) != 0U) {
        const bool known = x > 0U && isDungeonExplored((uint8_t)map_floor, (uint8_t)(x - 1U), (uint8_t)y);
        fill_rect(frame, screen_x, screen_y + 3, 4, 2, known ? color : COLOR_ORANGE);
        if (!known) fill_rect(frame, screen_x, screen_y + 2, 2, 4, COLOR_ORANGE);
    }
}

static void drawMapKeyItem(uint16_t *frame, int y, uint16_t color, const char *label)
{
    fill_rect(frame, 174, y, 10, 10, color);
    drawPet2001TextTransparent(frame, 190, y + 1, label, COLOR_WHITE, 1);
}

static void drawMapSidePanel(uint16_t *frame)
{
    draw_line(frame, 164, 0, 164, 160, COLOR_DARKGRAY);
    drawPet2001TextTransparent(frame, 174, 5, "KEY", COLOR_CYAN, 2);
    drawMapKeyItem(frame, 31, COLOR_YELLOW, "PLR");
    drawMapKeyItem(frame, 47, COLOR_LIGHTGRAY, "PATH");
    drawMapKeyItem(frame, 63, COLOR_CYAN, "ROOM");
    drawMapKeyItem(frame, 79, COLOR_ORANGE, "UNEXP EXIT");
    drawMapKeyItem(frame, 95, COLOR_PURPLE, "STAIRS");
    drawMapKeyItem(frame, 111, COLOR_GREEN, "ENTRANCE");
    drawMapKeyItem(frame, 127, COLOR_RED, "KEY");

    drawPet2001TextCentredTransparent(frame, 247, 3, "N", COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 247, 35, "S", COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 232, 19, "W", COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 262, 19, "E", COLOR_WHITE, 1);
    draw_line(frame, 239, 23, 255, 23, COLOR_LIGHTGRAY);
    draw_line(frame, 247, 15, 247, 31, COLOR_LIGHTGRAY);
    draw_line(frame, 247, 13, 243, 20, COLOR_RED);
    draw_line(frame, 247, 13, 251, 20, COLOR_RED);

    draw_rect(frame, 270, 5, 40, 40, COLOR_WHITE);
    draw_line(frame, 290, 5, 290, 45, COLOR_WHITE);
    draw_line(frame, 270, 25, 310, 25, COLOR_WHITE);
    for (unsigned page = 0; page < 4U; ++page) {
        const int x = 270 + (int)(page % 2U) * 20;
        const int y = 5 + (int)(page / 2U) * 20;
        const bool selected = page == map_page;
        fill_rect(frame, x + 2, y + 2, 17, 17, selected ? COLOR_YELLOW : COLOR_DARKGRAY);
        char number[2] = { (char)('1' + page), '\0' };
        drawPet2001TextCentredTransparent(frame, x + 10, y + 6, number,
                                           selected ? COLOR_BLACK : COLOR_WHITE, 1);
    }
    const unsigned player_page = (player_position.y / 20U) * 2U + player_position.x / 20U;
    if (player_position.floor == map_floor && player_page != map_page) {
        const int x = 270 + (int)(player_page % 2U) * 20;
        const int y = 5 + (int)(player_page / 2U) * 20;
        fill_circle(frame, x + 15, y + 15, 2, COLOR_YELLOW);
    }
}

static void drawDungeonMap(uint16_t *frame)
{
    const unsigned first_column = (map_page % 2U) * 20U;
    const unsigned first_row = (map_page / 2U) * 20U;
    const DungeonFloor *floor = &getDungeonWorld()->floors[map_floor];
    clear(frame, COLOR_BLACK);
    for (unsigned y = first_row; y < first_row + 20U; ++y)
        for (unsigned x = first_column; x < first_column + 20U; ++x)
            if (isDungeonExplored((uint8_t)map_floor, (uint8_t)x, (uint8_t)y)) drawMapCell(frame, x, y, first_column, first_row, &floor->cells[y][x]);
    if ((getDungeonPlayerState()->runFlags & DUNGEON_RUN_KEY_REVEALED) != 0U) {
        bool key_drawn = false;
        for (unsigned y = 0; y < DUNGEON_HEIGHT && !key_drawn; ++y) {
            for (unsigned x = 0; x < DUNGEON_WIDTH; ++x) {
                const DungeonCell *cell = &floor->cells[y][x];
                if ((cell->eventFlags & DUNGEON_EVENT_KEY) == 0U ||
                    (cell->stateFlags & DUNGEON_STATE_KEY_COLLECTED) != 0U) continue;
                if (x >= first_column && x < first_column + 20U &&
                    y >= first_row && y < first_row + 20U)
                    fill_rect(frame, ((int)x - (int)first_column) * 8 + 1,
                              ((int)y - (int)first_row) * 8 + 1, 6, 6, COLOR_RED);
                key_drawn = true;
                break;
            }
        }
    }
    if (player_position.floor == map_floor && player_position.x >= first_column && player_position.x < first_column + 20U && player_position.y >= first_row && player_position.y < first_row + 20U)
        fill_rect(frame, ((int)player_position.x - (int)first_column) * 8 + 1, ((int)player_position.y - (int)first_row) * 8 + 1, 6, 6, COLOR_YELLOW);
    drawMapSidePanel(frame);
    char footer[20];
    snprintf(footer, sizeof(footer), "FLOOR %u  PAGE %u/4", map_floor + 1U, map_page + 1U);
    drawPet2001TextTransparent(frame, 4, 161, footer, COLOR_WHITE, 1);
    drawPet2001TextTransparent(frame, 172, 161, "A/K PAGE  C CLOSE", COLOR_LIGHTGRAY, 1);
}

static bool isRoomTile(uint8_t tile_type)
{
    return tile_type == DUNGEON_TILE_ROOM_OPEN ||
           tile_type == DUNGEON_TILE_DEAD_END_ROOM_ROUND ||
           tile_type == DUNGEON_TILE_DEAD_END_ROOM_SQUARE ||
           tile_type == DUNGEON_TILE_DEAD_END_ROOM_ROUND_ALT;
}

static unsigned exploredRoomCount(unsigned floor_index, unsigned *total_rooms)
{
    unsigned explored = 0;
    unsigned total = 0;
    for (unsigned y = 0; y < DUNGEON_HEIGHT; ++y)
        for (unsigned x = 0; x < DUNGEON_WIDTH; ++x)
            if (isRoomTile(getDungeonWorld()->floors[floor_index].cells[y][x].tileType)) {
                ++total;
                if (isDungeonExplored((uint8_t)floor_index, (uint8_t)x, (uint8_t)y)) ++explored;
            }
    if (total_rooms != NULL) *total_rooms = total;
    return explored;
}

static unsigned specialItemCount(uint32_t flags)
{
    unsigned count = 0;
    while (flags != 0U) {
        count += flags & 1U;
        flags >>= 1;
    }
    return count;
}

static void formatPlayedTime(char *text, size_t size, uint32_t total_seconds)
{
    const uint32_t hours = total_seconds / 3600U;
    const uint32_t minutes = (total_seconds / 60U) % 60U;
    const uint32_t seconds = total_seconds % 60U;
    snprintf(text, size, "%lu:%02lu:%02lu", (unsigned long)hours,
             (unsigned long)minutes, (unsigned long)seconds);
}

/* -------------------------------------------------------------------------- */
/* Status, rewards, traps, shops, and combat screens                          */
/* -------------------------------------------------------------------------- */
static void drawStatusScreen(uint16_t *frame)
{
    const DungeonPlayerState *player = getDungeonPlayerState();
    char text[40];
    clear(frame, COLOR_BLACK);
    draw_text_centred(frame, 160, 5, status_page == 0U ? "STATUS" : status_page == 1U ? "INVENTORY" : "PROGRESS", 2, COLOR_CYAN);
    snprintf(text, sizeof(text), "%u/3", status_page + 1U);
    draw_text(frame, 292, 7, text, 1, COLOR_DARKGRAY);
    if (status_page == 0U) {
        snprintf(text, sizeof(text), "Lvl %u  XP %lu/%lu", (unsigned)player->level, (unsigned long)player->experience, (unsigned long)dungeonExperienceNeeded(player->level)); draw_text(frame, 12, 35, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Health: %u/%u", (unsigned)player->health, (unsigned)player->maxHealth); draw_text(frame, 12, 58, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Attack: %u", (unsigned)player->attack); draw_text(frame, 12, 81, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Defence: %u", (unsigned)player->defence); draw_text(frame, 12, 104, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Level choices: %u", (unsigned)player->unspentStatPoints); draw_text(frame, 12, 127, text, 2, COLOR_WHITE);
    } else if (status_page == 1U) {
        snprintf(text, sizeof(text), "Gold: %lu", (unsigned long)player->gold); draw_text(frame, 12, 40, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Keys: %u", (unsigned)player->keys); draw_text(frame, 12, 67, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Health potions: %u", (unsigned)player->healthPotions); draw_text(frame, 12, 94, text, 2, COLOR_WHITE);
        snprintf(text, sizeof(text), "Special items: %u", specialItemCount(player->inventoryFlags)); draw_text(frame, 12, 121, text, 2, COLOR_WHITE);
    } else {
        snprintf(text, sizeof(text), "MOVES %lu", (unsigned long)player->moves);
        draw_text_centred(frame, 160, 26, text, 2, COLOR_CYAN);
        formatPlayedTime(text, sizeof(text), player->timePlayedSeconds);
        draw_text_centred(frame, 160, 47, text, 2, COLOR_WHITE);
        for (unsigned floor = 0; floor < DUNGEON_FLOOR_COUNT; ++floor) {
            unsigned total_rooms = 0U;
            const unsigned explored_rooms = exploredRoomCount(floor, &total_rooms);
            snprintf(text, sizeof(text), "Floor %u: %u/%u rooms", floor + 1U, explored_rooms, total_rooms);
            draw_text_centred(frame, 160, 66 + (int)floor * 22, text, 2, COLOR_WHITE);
        }
    }
    draw_text_centred(frame, 160, 153, status_page == 0U && player->unspentStatPoints > 0U ? "A/K NEXT  C BONUS  E CLOSE" : "A/K NEXT  E CLOSE  BOOT BACK", 1, COLOR_LIGHTGRAY);
}

static void drawRewardScreen(uint16_t *frame)
{
    if (!image_visible) clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 31, reward_is_key ? "KEY" : "TREASURE",
                                       reward_is_key ? COLOR_CYAN : COLOR_YELLOW, 3);
    drawPet2001TextCentredTransparent(frame, 160, 80, reward_text, COLOR_WHITE, 2);
    drawPet2001TextCentredTransparent(frame, 160, 145, "A CONTINUE", COLOR_LIGHTGRAY, 1);
}

static void drawTrapResultScreen(uint16_t *frame)
{
    char text[40];
    if (!image_visible) clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 8, trap_defeated ? "DEFEATED" : "TRAP!",
                                       trap_defeated ? COLOR_RED : COLOR_YELLOW, 2);
    if (trap_defeated) snprintf(text, sizeof(text), "RETURNED WITH HALF HEALTH");
    else snprintf(text, sizeof(text), "DAMAGE %u", (unsigned)trap_damage);
    drawPet2001TextCentredTransparent(frame, 160, 137, text, COLOR_YELLOW, 1);
    drawPet2001TextCentredTransparent(frame, 160, 153, "A CONTINUE", COLOR_WHITE, 1);
}

static void drawLockedDoorScreen(uint16_t *frame)
{
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 37, "LOCKED DOOR", COLOR_RED, 3);
    drawPet2001TextCentredTransparent(frame, 160, 85, "FIND THE KEY", COLOR_YELLOW, 2);
    drawPet2001TextCentredTransparent(frame, 160, 145, "A RETURN", COLOR_LIGHTGRAY, 1);
}

static void drawLevelBonusScreen(uint16_t *frame)
{
    static const char *options[] = { "HEALTH", "ATTACK", "DEFENCE" };
    const DungeonPlayerState *player = getDungeonPlayerState();
    char text[40];
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 16, "LEVEL BONUS", COLOR_CYAN, 2);
    snprintf(text, sizeof(text), "CHOICES %u", (unsigned)player->unspentStatPoints);
    drawPet2001TextCentredTransparent(frame, 160, 47, text, COLOR_WHITE, 1);
    for (unsigned index = 0; index < 3U; ++index)
        drawPet2001TextCentredTransparent(frame, 160, 70 + (int)index * 18, options[index],
                                           index == selected_stat_option ? COLOR_YELLOW : COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 160, 153, "UP/DOWN SELECT  A CHOOSE  E BACK",
                                       COLOR_LIGHTGRAY, 1);
}

static void drawShopScreen(uint16_t *frame)
{
    static const char *labels[] = { "RESTORE HP", "MAX HP +", "ATTACK +", "DEFENCE +", "LEAVE SHOP" };
    char text[40];
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 8, "SHOP", COLOR_CYAN, 2);
    snprintf(text, sizeof(text), "GOLD %lu", (unsigned long)getDungeonPlayerState()->gold);
    drawPet2001TextCentredTransparent(frame, 160, 29, text, COLOR_YELLOW, 1);
    for (unsigned index = 0; index < 5U; ++index) {
        if (index < 4U) snprintf(text, sizeof(text), "%s  %lu", labels[index], (unsigned long)shopChoiceCost(index));
        else snprintf(text, sizeof(text), "%s", labels[index]);
        drawPet2001TextCentredTransparent(frame, 160, 48 + (int)index * 17, text,
                                           index == selected_shop_option ? COLOR_YELLOW : COLOR_WHITE, 1);
    }
    if (shop_text[0] != '\0') {
        drawPet2001TextCentredTransparent(frame, 160, 137, shop_text, COLOR_CYAN, 1);
    }
    drawPet2001TextCentredTransparent(frame, 160, 153, "UP/DOWN SELECT  A BUY  E EXIT",
                                       COLOR_LIGHTGRAY, 1);
}

static void drawCombatScreen(uint16_t *frame)
{
    static const char *options[] = { "ATTACK", "RUN", "POTION" };
    const DungeonPlayerState *player = getDungeonPlayerState();
    char text[40];
    if (!image_visible) clear(frame, COLOR_BLACK);
    draw_text_centred(frame, 160, 5, combatEnemyName(combat_state.enemyType), 2, COLOR_RED);
    draw_text(frame, 7, 34, "YOU", 1, COLOR_CYAN);
    snprintf(text, sizeof(text), "HP %u/%u", (unsigned)player->health, (unsigned)player->maxHealth); draw_text(frame, 7, 49, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "ATT %u", (unsigned)player->attack); draw_text(frame, 7, 63, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "DEF %u", (unsigned)player->defence); draw_text(frame, 7, 77, text, 1, COLOR_WHITE);
    draw_text(frame, 238, 34, "ENEMY", 1, COLOR_RED);
    snprintf(text, sizeof(text), "HP %u/%u", (unsigned)combat_state.health, (unsigned)combat_state.maxHealth); draw_text(frame, 238, 49, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "ATT %u", (unsigned)combat_state.attack); draw_text(frame, 238, 63, text, 1, COLOR_WHITE);
    snprintf(text, sizeof(text), "DEF %u", (unsigned)combat_state.defence); draw_text(frame, 238, 77, text, 1, COLOR_WHITE);
    if (combat_text[0] != '\0') draw_text_centred(frame, 160, 96, combat_text, 1, COLOR_YELLOW);
    for (unsigned index = 0; index < combatOptionCount(); ++index)
        draw_text_centred(frame, 160, 110 + (int)index * 14, options[index], 1, index == selected_combat_option ? COLOR_YELLOW : COLOR_WHITE);
    draw_text_centred(frame, 160, 156, "UP/DOWN SELECT  A CHOOSE", 1, COLOR_LIGHTGRAY);
}

static void drawCombatResultScreen(uint16_t *frame)
{
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 37,
                                       combat_result_title == NULL ? "COMBAT" : combat_result_title,
                                       combat_result_color, 3);
    drawPet2001TextCentredTransparent(frame, 160, 88, combat_text, COLOR_WHITE, 1);
    drawPet2001TextCentredTransparent(frame, 160, 145, "A CONTINUE", COLOR_LIGHTGRAY, 1);
}

static void drawCompleteScreen(uint16_t *frame)
{
    const DungeonPlayerState *player = getDungeonPlayerState();
    char text[40];
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 18, "YOU WIN", COLOR_GREEN, 3);
    snprintf(text, sizeof(text), "LEVEL %u  XP %lu", (unsigned)player->level, (unsigned long)player->experience);
    drawPet2001TextCentredTransparent(frame, 160, 67, text, COLOR_WHITE, 1);
    snprintf(text, sizeof(text), "GOLD %lu  MOVES %lu", (unsigned long)player->gold, (unsigned long)player->moves);
    drawPet2001TextCentredTransparent(frame, 160, 89, text, COLOR_YELLOW, 1);
    if (history_result.error == DUNGEON_HISTORY_OK && completion_rank > 0U) {
        snprintf(text, sizeof(text), "HALL OF FAME #%u", (unsigned)completion_rank);
        drawPet2001TextCentredTransparent(frame, 160, 112, text, COLOR_CYAN, 1);
    }
    drawPet2001TextCentredTransparent(frame, 160, 145, "A RETURN TO GAMES", COLOR_LIGHTGRAY, 1);
}

static void drawHistoryScreen(uint16_t *frame)
{
    char text[40];
    clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 7, "DUNGEON HALL OF FAME", COLOR_CYAN, 2);
    if (history_result.error != DUNGEON_HISTORY_OK) {
        const char *error_text = dungeonHistoryErrorText(history_result.error);
        // PET at 2x fits 20 cells; retain longer checksum diagnostics in full.
        const int error_scale = strlen(error_text) > 20U ? 1 : 2;
        drawPet2001TextCentredTransparent(frame, 160, 76, error_text, COLOR_YELLOW, error_scale);
    } else if (dungeonHistoryCount() == 0U) {
        drawPet2001TextCentredTransparent(frame, 160, 76, "NO COMPLETIONS YET", COLOR_YELLOW, 2);
    } else {
        for (unsigned index = 0; index < dungeonHistoryCount(); ++index) {
            const DungeonHistoryRecord *record = dungeonHistoryRecord((uint8_t)index);
            snprintf(text, sizeof(text), "%u  %luG  L%u  %luM", index + 1U,
                     (unsigned long)record->gold, (unsigned)record->level, (unsigned long)record->moves);
            drawPet2001TextCentredTransparent(frame, 160, 32 + (int)index * 21, text,
                                               index == 0U ? COLOR_YELLOW : COLOR_WHITE, 1);
        }
    }
    drawPet2001TextCentredTransparent(frame, 160, 153, "A RETURN TO MENU  BOOT BACK",
                                       COLOR_LIGHTGRAY, 1);
}

/* -------------------------------------------------------------------------- */
/* Top-level Dungeon renderer                                                 */
/* -------------------------------------------------------------------------- */
void drawDungeon(uint16_t *frame)
{
    if (!running || frame == NULL) return;
    if (state == DUNGEON_COMBAT) {
        drawCombatScreen(frame);
        return;
    }
    if (state == DUNGEON_COMBAT_RESULT) {
        drawCombatResultScreen(frame);
        return;
    }
    if (state == DUNGEON_COMPLETE) {
        drawCompleteScreen(frame);
        return;
    }
    if (state == DUNGEON_HISTORY) {
        drawHistoryScreen(frame);
        return;
    }
    if (state == DUNGEON_TRAP_RESULT) {
        drawTrapResultScreen(frame);
        return;
    }
    if (state == DUNGEON_LOCKED_DOOR) {
        drawLockedDoorScreen(frame);
        return;
    }
    if (state == DUNGEON_LEVEL_BONUS) {
        drawLevelBonusScreen(frame);
        return;
    }
    if (state == DUNGEON_SHOP) {
        drawShopScreen(frame);
        return;
    }
    if (state == DUNGEON_REWARD) {
        drawRewardScreen(frame);
        return;
    }
    if (state == DUNGEON_ROOM && status_visible) {
        drawStatusScreen(frame);
        return;
    }
    if (state == DUNGEON_ROOM && map_visible) {
        drawDungeonMap(frame);
        return;
    }
    if (state == DUNGEON_ROOM && decode_pending) return;
    if (state == DUNGEON_ROOM && image_visible) {
        const DungeonCell *cell = &getDungeonWorld()->floors[player_position.floor].cells[player_position.y][player_position.x];
        if ((cell->exits & DUNGEON_EXIT_NORTH) != 0U) drawExitIcon(frame, DUNGEON_EXIT_NORTH, exitDestinationExplored(DUNGEON_EXIT_NORTH) ? COLOR_GREEN : COLOR_CYAN);
        if ((cell->exits & DUNGEON_EXIT_EAST) != 0U) drawExitIcon(frame, DUNGEON_EXIT_EAST, exitDestinationExplored(DUNGEON_EXIT_EAST) ? COLOR_GREEN : COLOR_CYAN);
        if ((cell->exits & DUNGEON_EXIT_SOUTH) != 0U) drawExitIcon(frame, DUNGEON_EXIT_SOUTH, exitDestinationExplored(DUNGEON_EXIT_SOUTH) ? COLOR_GREEN : COLOR_CYAN);
        if ((cell->exits & DUNGEON_EXIT_WEST) != 0U) drawExitIcon(frame, DUNGEON_EXIT_WEST, exitDestinationExplored(DUNGEON_EXIT_WEST) ? COLOR_GREEN : COLOR_CYAN);
        if (stairAvailable(true)) drawStairIcon(frame, true);
        if (stairAvailable(false)) drawStairIcon(frame, false);
        if (isShopRoom(cell)) drawShopIndicator(frame);
        if (ai_mode) {
            char ai_text[28];
            snprintf(ai_text, sizeof(ai_text), "AI %s  E SPEED", aiSpeedShortText(ai_speed_index));
            draw_text_centred(frame, 160, 156, ai_text, 1, COLOR_YELLOW);
        }
        return;
    }
    if (state == DUNGEON_START_MENU && decode_pending) {
        // Avoid showing the previous screen during the one-frame JPEG decode.
        clear(frame, COLOR_BLACK);
        return;
    }
    // PET start-menu text is transparent, so retain the decoded JPEG beneath it.
    if (state != DUNGEON_START_MENU || !image_visible) clear(frame, COLOR_BLACK);
    drawPet2001TextCentredTransparent(frame, 160, 10, "DUNGEON", COLOR_CYAN, 3);
    if (state == DUNGEON_CREATING) {
        const char *checkpoint = "READY TO PLAY";
        if (creation_step == CREATE_DELETE) checkpoint = "CLEARING OLD FILES";
        else if (creation_step == CREATE_GENERATE) checkpoint = "MAP / ROOMS / KEYS";
        else if (creation_step == CREATE_WORLD) checkpoint = "SAVING DUNGEON MAP";
        else if (creation_step == CREATE_PLAYER) checkpoint = "PLAYER / LEVEL";
        else if (creation_step == CREATE_EXPLORED) checkpoint = "EXPLORING MAP";
        drawPet2001TextCentredTransparent(frame, 160, 43, "CREATING NEW DUNGEON", COLOR_WHITE, 2);
        drawPet2001TextCentredTransparent(frame, 160, 82, checkpoint, COLOR_CYAN, 2);
        drawPet2001TextCentredTransparent(frame, 160, 120, "PLEASE WAIT", COLOR_LIGHTGRAY, 1);
    } else if (storage_result.error != DUNGEON_STORAGE_OK) {
        drawPet2001TextCentredTransparent(frame, 160, 62, dungeonStorageErrorText(storage_result.error), COLOR_RED, 2);
        drawPet2001TextCentredTransparent(frame, 160, 88, "/spiffs/dungeon.dat", COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 116, creation_trace[0] != '\0' ? creation_trace : "A ERASES + CREATES NEW", COLOR_YELLOW, 1);
    } else if (state == DUNGEON_AI_START) {
        char countdown[20];
        drawPet2001TextCentredTransparent(frame, 160, 38, "AI MODE", COLOR_CYAN, 3);
        drawPet2001TextCentredTransparent(frame, 160, 80, "NEW DUNGEON", COLOR_WHITE, 2);
        snprintf(countdown, sizeof(countdown), "STARTING IN %u", (unsigned)ai_start_seconds);
        drawPet2001TextCentredTransparent(frame, 160, 124, countdown, COLOR_LIGHTGRAY, 1);
    } else if (state == DUNGEON_START_MENU) {
        for (unsigned index = 0; index < sizeof(start_options) / sizeof(start_options[0]); ++index) {
            drawPet2001TextCentredTransparent(frame, 160, 43 + (int)index * 21, start_options[index],
                                               index == selected_option ? COLOR_CYAN : COLOR_WHITE,
                                               index == selected_option ? 2 : 1);
        }
    } else if (state == DUNGEON_NOTICE) {
        const char *notice_text = player_result.error != DUNGEON_PLAYER_OK ? dungeonPlayerErrorText(player_result.error) : explored_result.error != DUNGEON_EXPLORED_OK ? dungeonExploredErrorText(explored_result.error) : selected_option == 1U ? "NEW DUNGEON NEXT" : selected_option == 2U ? "AI MODE NEXT" : "BOOT RETURNS TO GAMES";
        // PET at 2x fits 20 cells. Keep longer storage/player diagnostics visible.
        const int notice_scale = strlen(notice_text) > 20U ? 1 : 2;
        drawPet2001TextCentredTransparent(frame, 160, 67, notice_text, COLOR_YELLOW, notice_scale);
        drawPet2001TextCentredTransparent(frame, 160, 92, creation_trace, COLOR_WHITE, 1);
        drawPet2001TextCentredTransparent(frame, 160, 116, "A RETURNS TO MENU", COLOR_WHITE, 1);
    } else {
        drawPet2001TextCentredTransparent(frame, 160, 75, "ENTRANCE IMAGE FAILED", COLOR_RED, 1);
        drawPet2001TextCentredTransparent(frame, 160, 104, tile_path, COLOR_WHITE, 1);
    }
    if (state != DUNGEON_CREATING) {
        drawPet2001TextCentredTransparent(frame, 160, 156, "UP/DOWN SELECT  A OPEN  BOOT BACK",
                                           COLOR_LIGHTGRAY, 1);
    }
}