From 00bc932ffdc8a4af6ed421eb78b11c4031177ec2 Mon Sep 17 00:00:00 2001 From: Vannevar X Date: Sat, 20 Jun 2026 11:29:22 -0400 Subject: [PATCH 1/3] Refactored Window_BattleStatus::Refresh and Window::BattleStatus.RefreshGauge to update incrementally, or at least to skip redrawing content when not needed. The battle status window is no longer cleared by a call to Refresh(), but full-clear-and-redraw functionality would be very easy to add if needed. In order to accomplish the incremental updates, Window_BattleStatus keeps a cache of the displayed values for each item, updating the graphics if those values change. The UIs for RM2k3 battle type B (alternative) and C (gauge) both have overlapping content, meaning multiple items need to be redrawn if one of the overlapping items is redrawn in the UI, so further optimization would be possible there. The function DrawGauge, which draws the gauges for battle types A (traditional) and B (alternative), has been moved from Window_Base into Window_BattleStatus, as that seems to be the only code that uses it. --- src/window_base.cpp | 32 ---- src/window_base.h | 1 - src/window_battlestatus.cpp | 332 ++++++++++++++++++++++++++---------- src/window_battlestatus.h | 81 ++++++++- 4 files changed, 319 insertions(+), 127 deletions(-) diff --git a/src/window_base.cpp b/src/window_base.cpp index 5c0c436821..59104e4f79 100644 --- a/src/window_base.cpp +++ b/src/window_base.cpp @@ -302,38 +302,6 @@ void Window_Base::DrawCurrencyValue(int money, int cx, int cy) const { contents->TextDraw(cx - gold_text_size.width, cy, Font::ColorDefault, gold.str(), Text::AlignRight); } -void Window_Base::DrawGauge(const Game_Battler& actor, int cx, int cy, int alpha) const { - BitmapRef system2 = Cache::System2(); - if (!system2) { - return; - } - - bool full = actor.IsAtbGaugeFull(); - - // Which gauge (0 - 2) - int gauge_y = 32 + 2 * 16; - - // Three components of the gauge - Rect gauge_left(0, gauge_y, 16, 16); - Rect gauge_center(16, gauge_y, 16, 16); - Rect gauge_right(32, gauge_y, 16, 16); - - Rect dst_rect(cx + 16, cy, 25, 16); - - contents->Blit(cx + 0, cy, *system2, gauge_left, alpha); - contents->Blit(cx + 16 + 25, cy, *system2, gauge_right, alpha); - contents->StretchBlit(dst_rect, *system2, gauge_center, alpha); - - const auto atb = actor.GetAtbGauge(); - const auto gauge_w = 25 * atb / actor.GetMaxAtbGauge(); - if (gauge_w > 0) { - // Full or not full bar - Rect gauge_bar(full ? 64 : 48, gauge_y, 16, 16); - Rect bar_rect(cx + 16, cy, gauge_w, 16); - contents->StretchBlit(bar_rect, *system2, gauge_bar, alpha); - } -} - void Window_Base::DrawActorHpValue(const Game_Battler& actor, int cx, int cy) const { contents->TextDraw(cx, cy, GetValueFontColor(actor.GetHp(), actor.GetMaxHp(), true), std::to_string(actor.GetHp()), Text::AlignRight); } diff --git a/src/window_base.h b/src/window_base.h index 68afdd94bc..13542be639 100644 --- a/src/window_base.h +++ b/src/window_base.h @@ -67,7 +67,6 @@ class Window_Base : public Window { void DrawItemName(const lcf::rpg::Item& item, int cx, int cy, bool enabled = true) const; void DrawSkillName(const lcf::rpg::Skill& skill, int cx, int cy, bool enabled = true) const; void DrawCurrencyValue(int money, int cx, int cy) const; - void DrawGauge(const Game_Battler& actor, int cx, int cy, int alpha = 255) const; void DrawActorHpValue(const Game_Battler& actor, int cx, int cy) const; void DrawActorSpValue(const Game_Battler& actor, int cx, int cy) const; int GetValueFontColor(int have, int max, bool can_knockout) const; diff --git a/src/window_battlestatus.cpp b/src/window_battlestatus.cpp index f3be182ff7..bd86c3dd07 100644 --- a/src/window_battlestatus.cpp +++ b/src/window_battlestatus.cpp @@ -31,6 +31,7 @@ #include "window_battlestatus.h" #include "feature.h" + Window_BattleStatus::Window_BattleStatus(int ix, int iy, int iwidth, int iheight, bool enemy) : Window_Selectable(ix, iy, iwidth, iheight), mode(ChoiceMode_All), enemy(enemy) { @@ -57,13 +58,21 @@ Window_BattleStatus::Window_BattleStatus(int ix, int iy, int iwidth, int iheight SetContents(Bitmap::Create(width, height)); SetOpacity(0); } - + isEmpty = true; Refresh(); } void Window_BattleStatus::Refresh() { - contents->Clear(); - + // If the battle system uses gauges, wait for system2 to load. + if (!Feature::HasRpg2kBattleSystem() && !Cache::System2() ) { + return; + } + // This avoids an issue where smaller face graphics (found in the game Ara Fell, as mentioned below in RefreshGauge) + // will be stretched to fill the full face area, before the window is visible. I am not sure why that happens. + if (!IsVisible()) { + return; + } + if (enemy) { item_max = Main_Data::game_enemyparty->GetBattlerCount(); } @@ -83,112 +92,252 @@ void Window_BattleStatus::Refresh() { actor = &(*Main_Data::game_party)[i]; } - if (!enemy && lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_gauge) { - DrawActorFace(*static_cast(actor), 80 * i, actor_face_height); + // If the actor ID for this item has changed, clear the graphics and reset the cached data + if (itemStates[i].actor_id != actor->GetId()) { + if (!isEmpty) { ClearItemGraphics(i); } + itemStates[i].reset(); + itemStates[i].actor_id = actor->GetId(); } - else { - int y = menu_item_height / 8 + i * menu_item_height; - DrawActorName(*actor, 4, y); - if (Feature::HasRpg2kBattleSystem()) { + bool hp_changed = itemStates[i].hp != actor->GetHp(); + bool hp_max_changed = itemStates[i].max_hp != actor->GetMaxHp(); + bool sp_changed = itemStates[i].sp != actor->GetSp(); + bool sp_max_changed = itemStates[i].max_sp != actor->GetMaxSp(); + + if (lcf::Data::battlecommands.battle_type != lcf::rpg::BattleCommands::BattleType_gauge || enemy) { + int y = menu_item_height / 8 + i * menu_item_height; + + const lcf::rpg::State* state = actor->GetSignificantState(); + bool state_changed = (state == NULL && itemStates[i].state_name.compare("") != 0 || itemStates[i].state_color != 0); + state_changed = state_changed || (state != NULL && (itemStates[i].state_name.compare(ToString(state->name)) != 0 || itemStates[i].state_color != state->color)); + + + int state_x; + + if (Feature::HasRpg2kBattleSystem() && (hp_changed || hp_max_changed || sp_changed || sp_max_changed || state_changed)) { int hpdigits = (actor->MaxHpValue() >= 1000) ? 4 : 3; int spdigits = (actor->MaxSpValue() >= 1000) ? 4 : 3; - DrawActorState(*actor, (hpdigits < 4 && spdigits < 4) ? 86 : 80, y); + state_x = (hpdigits < 4 && spdigits < 4) ? 86 : 80; + int sp_x = 220 - spdigits * 6; + + // Just clear and redraw the whole Status/HP/MP area for now when something changes + if (!isEmpty) { contents->ClearRect(Rect(state_x, y, sp_x - state_x, menu_item_height)); } + DrawActorState(*actor, state_x, y); DrawActorHp(*actor, 178 - hpdigits * 6 - spdigits * 6, y, hpdigits, true); - DrawActorSp(*actor, 220 - spdigits * 6, y, spdigits, false); - } else { + DrawActorSp(*actor, sp_x, y, spdigits, false); + + } else if (!Feature::HasRpg2kBattleSystem() && (hp_changed || hp_max_changed || state_changed)) { + // BattleType_traditional/Type A if (lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_traditional) { - DrawActorState(*actor, 84, y); - DrawActorHpValue(*actor, 136 + 4 * 6, y); + state_x = 84; + int hp_x = 136 + 4 * 6; + // State is left aligned and HP is right aligned, so redraw both of them due to the variable amount of space between them. + if (!isEmpty) { contents->ClearRect(Rect(state_x, y, hp_x - state_x, menu_item_height)); } + DrawActorState(*actor, state_x, y); + DrawActorHpValue(*actor, hp_x, y); + // BattleType_alternative/Type B } else { - DrawActorState(*actor, 80, y); + state_x = 80; + if (state_changed) { + // Clear up to the gauge area + if (!isEmpty) { contents->ClearRect(Rect(state_x, y, 130 - state_x, menu_item_height)); } + DrawActorState(*actor, state_x, y); + } } } + + if (itemStates[i].actor_name.compare(actor->GetName())) { + if (!isEmpty) { contents->ClearRect(Rect(4, y, state_x - 4, menu_item_height)); } + DrawActorName(*actor, 4, y); + itemStates[i].actor_name = actor->GetName(); + } + itemStates[i].state_name = state ? ToString(state->name) : ""; + itemStates[i].state_color = state ? state->color : 0; + } + + RefreshGauge(actor, i, hp_changed, hp_max_changed, sp_changed, sp_max_changed); + + itemStates[i].hp = actor->GetHp(); + itemStates[i].max_hp = actor->GetMaxHp(); + itemStates[i].sp = actor->GetSp(); + itemStates[i].max_sp = actor->GetMaxSp(); + + itemStates[i].is_drawn = true; + } + for (int i = item_max; i < 4; i++) { + if (itemStates[i].is_drawn) { + ClearItemGraphics(i); + itemStates[i].reset(); } } + isEmpty = false; +} - RefreshGauge(); +void Window_BattleStatus::ClearItemGraphics(int i_item) { + if (lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_gauge) { + contents->ClearRect(Rect(80 * i_item, 0, 80, height)); + } + else { + contents->ClearRect(Rect(0, menu_item_height / 8 + i_item * menu_item_height, width, menu_item_height)); + } + } -void Window_BattleStatus::RefreshGauge() { - if (Feature::HasRpg2k3BattleSystem()) { - if (lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_alternative) { - if (lcf::Data::battlecommands.window_size == lcf::rpg::BattleCommands::WindowSize_small) { - contents->ClearRect(Rect(192, 0, 45, 58)); - } else { - contents->ClearRect(Rect(192, 0, 45, 64)); - } - } +void Window_BattleStatus::RefreshGauge(const Game_Battler* actor, int i_item, bool hp_changed, bool hp_max_changed, bool sp_changed, bool sp_max_changed) { - for (int i = 0; i < item_max; ++i) { - // The party always contains valid battlers - Game_Battler* actor; - if (enemy) { - actor = &(*Main_Data::game_enemyparty)[i]; - } - else { - actor = &(*Main_Data::game_party)[i]; + if (!Feature::HasRpg2k3BattleSystem()) { + return; + } + + bool atb_bar_full = actor->IsAtbGaugeFull(); + int atb_bar_w; + + // There are multiple gauges to draw for BattleType_gauge/Type C + if (!enemy && lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_gauge) { + BitmapRef system2 = Cache::System2(); + assert(system2); + + atb_bar_w = GaugeWidthSystem2(actor->GetAtbGauge(), actor->GetMaxAtbGauge()); + const int x_start = 32 + i_item * 80; + const int fill_x = x_start + 16; + int y = actor_face_height; + + const Game_Actor* game_actor = static_cast(actor); + bool face_changed = game_actor->GetFaceName().compare(itemStates[i_item].face_name) != 0 || game_actor->GetFaceIndex() != itemStates[i_item].face_index; + // If the HP or SP have changed, 4-digit numbers can overlap the face graphic. The face graphic + // is also overlapped by the gauge graphics, so just redraw everything. (also do this if the ATB gauge resets.) + // Could be fine-tuned, but performance seems ok for now. + if (hp_changed || hp_max_changed || sp_changed || sp_max_changed || face_changed || atb_bar_w < itemStates[i_item].atb_bar_width) { + // Clear number and gauge drawing area + if (!isEmpty) { + contents->ClearRect(Rect(40 + 80 * i_item, actor_face_height, 8 * 4, 48)); } + + // Note that both clear and redraw are needed because some games don't have + // face graphics that are huge enough to clear the left side of the number area (e.g. Ara Fell) + DrawActorFace(*static_cast(actor), 80 * i_item, actor_face_height); + + int x = x_start; + + // Left Gauge Segments + contents->Blit(x, y, *system2, Rect(0, 32, 16, 48), Opacity::Opaque()); + x += 16; + + // Center + contents->StretchBlit(Rect(x, y, 25, 48), *system2, Rect(16, 32, 16, 48), Opacity::Opaque()); + x += 25; + + // Right + contents->Blit(x, y, *system2, Rect(32, 32, 16, 48), Opacity::Opaque()); + + // HP + DrawGaugeSystem2(fill_x, y, actor->GetHp(), actor->GetMaxHp(), 0); + // SP + DrawGaugeSystem2(fill_x, y + 16, actor->GetSp(), actor->GetMaxSp(), 1); + // ATB + DrawGaugeSystem2(fill_x, y + 16 * 2, actor->GetAtbGauge(), actor->GetMaxAtbGauge(), 2); + + // Numbers + x = 40 + 80 * i_item; + DrawNumberSystem2(x, y, actor->GetHp()); + DrawNumberSystem2(x, y + 12 + 4, actor->GetSp()); + + itemStates[i_item].face_name = game_actor->GetFaceName(); + itemStates[i_item].face_index = game_actor->GetFaceIndex(); + } + // If only the ATB gauge has only updated (common), we can get away with only drawing the gauge bar + else if (atb_bar_w > itemStates[i_item].atb_bar_width || atb_bar_full != itemStates[i_item].atb_bar_full) { + DrawGaugeSystem2(fill_x, y + 16 * 2, actor->GetAtbGauge(), actor->GetMaxAtbGauge(), 2); + } + itemStates[i_item].atb_bar_width = atb_bar_w; + itemStates[i_item].atb_bar_full = atb_bar_full; + } + else { + int y = menu_item_height / 8 + i_item * menu_item_height; + atb_bar_w = GaugeWidth(actor->GetAtbGauge(), actor->GetMaxAtbGauge()); - if (!enemy && lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_gauge) { - BitmapRef system2 = Cache::System2(); - if (system2) { - // Clear number and gauge drawing area - contents->ClearRect(Rect(40 + 80 * i, actor_face_height, 8 * 4, 48)); - - // Number clearing removed part of the face, but both, clear and redraw - // are needed because some games don't have face graphics that are huge enough - // to clear the number area (e.g. Ara Fell) - DrawActorFace(*static_cast(actor), 80 * i, actor_face_height); - - int x = 32 + i * 80; - int y = actor_face_height; - - // Left Gauge - contents->Blit(x, y, *system2, Rect(0, 32, 16, 48), Opacity::Opaque()); - x += 16; - - // Center - const auto fill_x = x; - contents->StretchBlit(Rect(x, y, 25, 48), *system2, Rect(16, 32, 16, 48), Opacity::Opaque()); - x += 25; - - // Right - contents->Blit(x, y, *system2, Rect(32, 32, 16, 48), Opacity::Opaque()); - - // HP - DrawGaugeSystem2(fill_x, y, actor->GetHp(), actor->GetMaxHp(), 0); - // SP - DrawGaugeSystem2(fill_x, y + 16, actor->GetSp(), actor->GetMaxSp(), 1); - // Gauge - DrawGaugeSystem2(fill_x, y + 16 * 2, actor->GetAtbGauge(), actor->GetMaxAtbGauge(), 2); - - // Numbers - x = 40 + 80 * i; - DrawNumberSystem2(x, y, actor->GetHp()); - DrawNumberSystem2(x, y + 12 + 4, actor->GetSp()); + // BattleType_alternative/Type B has a lot of overlapping content, and is the trickiest one to optimize. + if (lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_alternative) { + int spdigits = (actor->MaxSpValue() >= 1000) ? 4 : 3; + bool has_opaque_gauge = lcf::Data::battlecommands.transparency == lcf::rpg::BattleCommands::Transparency_opaque || (menu_item_height / 8 + index * menu_item_height != y); + bool didUpdate = false; + // If any of these have changed, then full clear and redraw since there's so much overlap. + if (hp_changed || hp_max_changed || sp_changed || sp_max_changed || atb_bar_w < itemStates[i_item].atb_bar_width || has_opaque_gauge != itemStates[i_item].has_opaque_gauge) { + if (!isEmpty) { + contents->ClearRect(Rect(192, y, 107, menu_item_height)); + } + // RPG_RT Bug (?): Gauge hidden when selected due to transparency (wrong color when rendering) + if (has_opaque_gauge) { + DrawGauge(202 - 10, y - 2, actor->GetAtbGauge(), actor->GetMaxAtbGauge(), actor->IsAtbGaugeFull(), true, + lcf::Data::battlecommands.transparency == lcf::rpg::BattleCommands::Transparency_opaque ? 96 : 255); } + int hpdigits = (actor->MaxHpValue() >= 1000) ? 4 : 3; + DrawActorHp(*actor, 178 - hpdigits * 6 - spdigits * 6, y, hpdigits, true); + didUpdate = true; } - else { - int y = menu_item_height / 8 + i * menu_item_height; - - if (lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_alternative) { - // RPG_RT Bug (?): Gauge hidden when selected due to transparency (wrong color when rendering) - if (lcf::Data::battlecommands.transparency == lcf::rpg::BattleCommands::Transparency_opaque || (menu_item_height / 8 + index * menu_item_height != y)) { - DrawGauge(*actor, 202 - 10, y - 2, lcf::Data::battlecommands.transparency == lcf::rpg::BattleCommands::Transparency_opaque ? 96 : 255); - } - int hpdigits = (actor->MaxHpValue() >= 1000) ? 4 : 3; - int spdigits = (actor->MaxSpValue() >= 1000) ? 4 : 3; - DrawActorHp(*actor, 178 - hpdigits * 6 - spdigits * 6, y, hpdigits, true); - DrawActorSp(*actor, 220 - spdigits * 6, y, spdigits, false); - } else { - DrawGauge(*actor, 156, y - 2); + // Only the ATB gauge has increased + else if (atb_bar_w > itemStates[i_item].atb_bar_width || atb_bar_full != itemStates[i_item].atb_bar_full) { + if (has_opaque_gauge) { + DrawGauge(202 - 10, y - 2, actor->GetAtbGauge(), actor->GetMaxAtbGauge(), actor->IsAtbGaugeFull(), false, + lcf::Data::battlecommands.transparency == lcf::rpg::BattleCommands::Transparency_opaque ? 96 : 255); } + didUpdate = true; + } + // The SP number overlaps the gauge bar, so it needs to be redrawn if any drawing was done above. + if (didUpdate) { + DrawActorSp(*actor, 220 - spdigits * 6, y, spdigits, false); + itemStates[i_item].atb_bar_width = atb_bar_w; + itemStates[i_item].atb_bar_full = atb_bar_full; + } + itemStates[i_item].has_opaque_gauge = has_opaque_gauge; + } else { + // BattleType_traditional/Type A + if (atb_bar_w != itemStates[i_item].atb_bar_width || atb_bar_full != itemStates[i_item].atb_bar_full) { + bool shouldDrawGraphic = atb_bar_w < itemStates[i_item].atb_bar_width; + DrawGauge(156, y - 2, actor->GetAtbGauge(), actor->GetMaxAtbGauge(), actor->IsAtbGaugeFull(), shouldDrawGraphic); } + itemStates[i_item].atb_bar_width = atb_bar_w; + itemStates[i_item].atb_bar_full = atb_bar_full; } + } } +// This draws the ATB gauge for BattleType_Traditional (Type A) and BattleType_alternative (Type B) +void Window_BattleStatus::DrawGauge(int cx, int cy, int cur_value, int max_value, bool is_full, bool draw_graphic, int alpha) { + BitmapRef system2 = Cache::System2(); + assert(system2); + int gauge_w = GaugeWidth(cur_value, max_value); + + // Use the Y position for the gauge at index 2 + int gauge_y = 32 + 2 * 16; + + if (draw_graphic) { + // Three components of the gauge + Rect gauge_left(0, gauge_y, 16, 16); + Rect gauge_center(16, gauge_y, 16, 16); + Rect gauge_right(32, gauge_y, 16, 16); + + Rect dst_rect(cx + 16, cy, 25, 16); + + contents->Blit(cx + 0, cy, *system2, gauge_left, alpha); + contents->Blit(cx + 16 + 25, cy, *system2, gauge_right, alpha); + contents->StretchBlit(dst_rect, *system2, gauge_center, alpha); + } + + // Draw the bar + if (gauge_w > 0) { + // Full or not full bar + Rect gauge_bar(is_full ? 64 : 48, gauge_y, 16, 16); + Rect bar_rect(cx + 16, cy, gauge_w, 16); + contents->StretchBlit(bar_rect, *system2, gauge_bar, alpha); + } +} + +int Window_BattleStatus::GaugeWidth(int cur_value, int max_value) { + return 25 * cur_value / max_value; +} + void Window_BattleStatus::DrawGaugeSystem2(int x, int y, int cur_value, int max_value, int which) { BitmapRef system2 = Cache::System2(); assert(system2); @@ -205,13 +354,16 @@ void Window_BattleStatus::DrawGaugeSystem2(int x, int y, int cur_value, int max_ gauge_x = 0; } - int gauge_width = 25; + int gauge_width = GaugeWidthSystem2(cur_value, max_value); + + contents->StretchBlit(Rect(x, y, gauge_width, 16), *system2, Rect(48 + gauge_x, 32 + 16 * which, 16, 16), Opacity::Opaque()); +} +int Window_BattleStatus::GaugeWidthSystem2(int cur_value, int max_value) { if (max_value > 0) { - gauge_width = 25 * cur_value / max_value; + return 25 * cur_value / max_value; } - - contents->StretchBlit(Rect(x, y, gauge_width, 16), *system2, Rect(48 + gauge_x, 32 + 16 * which, 16, 16), Opacity::Opaque()); + return 25; } void Window_BattleStatus::DrawNumberSystem2(int x, int y, int value) { @@ -276,10 +428,8 @@ void Window_BattleStatus::Update() { item_max = Main_Data::game_party->GetBattlerCount(); } - if (item_max != old_item_max) { + if (item_max != old_item_max || Feature::HasRpg2k3BattleSystem()) { Refresh(); - } else if (Feature::HasRpg2k3BattleSystem()) { - RefreshGauge(); } if (active && index >= 0) { diff --git a/src/window_battlestatus.h b/src/window_battlestatus.h index 046beacdf6..dbf3b28bd0 100644 --- a/src/window_battlestatus.h +++ b/src/window_battlestatus.h @@ -77,11 +77,40 @@ class Window_BattleStatus : public Window_Selectable { void UpdateCursorRect() override; /** - * Redraws the characters time gauge. + * Clear the graphics for one character's row or face portrait area. + */ + void ClearItemGraphics(int i_item); + + /** + * Redraws the characters time gauge, as well as HP and SP if applicable. + */ + void RefreshGauge(const Game_Battler* actor, int i_item, bool hp_changed, bool hp_max_changed, bool sp_changed, bool sp_max_changed); + + /** + * Draw the time gauge for BattleType_traditional (A) and BattleType_alternative (B). + */ + void DrawGauge(int cx, int cy, int cur_value, int max_value, bool is_full, bool draw_graphic, int alpha = 255); + + /** + * Used to determine if the ATB gauge needs redrawing. + * + * @return the pixel width of the ATB gauge + */ + int GaugeWidth(int cur_value, int max_value); + + /** + * Draw the gauge bars for Battletype_gauge (C) */ - void RefreshGauge(); - void DrawGaugeSystem2(int x, int y, int cur_value, int max_value, int which); + /** + * Used to determine if the a gauge needs redrawing. + * + * @return the pixel width of the HP, SP, or ATB gauge + */ + int GaugeWidthSystem2(int cur_value, int max_value); + /** + * Draw the time gauge for Battletype_gauge (C) + */ void DrawNumberSystem2(int x, int y, int value); /** @@ -93,12 +122,58 @@ class Window_BattleStatus : public Window_Selectable { ChoiceMode mode; + bool isEmpty = true; + // Debug helper bool enemy; FileRequestBinding request_id; int actor_face_height = 24; + struct BattleItemState { + int i_item = 0; + + bool is_drawn = false; + //bool has_drawn_face = false; + int actor_id = -1; // Full redraw if this changes + std::string actor_name = ""; + + int hp = -1; + int max_hp = -1; + int sp = -1; + int max_sp = -1; + std::string state_name = ""; + int32_t state_color; + + std::string face_name = ""; + int face_index = -1; + + int atb_bar_width = INT_MAX; + bool atb_bar_full = false; + bool has_opaque_gauge = true; + + void reset() { + is_drawn = false; + //has_drawn_face = false; + actor_id = -1; + actor_name = ""; + hp = -1; + max_hp = -1; + sp = -1; + max_sp = -1; + state_name = ""; + state_color = 0; + atb_bar_width = INT_MAX; + atb_bar_full = false; + has_opaque_gauge = true; + face_name = ""; + face_index = -1; + } + }; + + // Contains the current displayed data in this view. This is compared with + // new data, in order to make incremental updates to the UI. + BattleItemState itemStates[4]; }; #endif From e30cc1bdb135f3d6738a2d46e4938653a4090ec0 Mon Sep 17 00:00:00 2001 From: Vannevar Xbox Date: Sat, 20 Jun 2026 13:39:37 -0400 Subject: [PATCH 2/3] Redraw the whole area if any actors have been added, removed, or changed, since it appears that at least in BattleType_alternative/B with a small window, the gauges can overlap into the row below. This makes sure the window gets redrawn properly, and this situation should be rare enough that this doesn't impact performance too much. Also, when re-drawing content in BattleType_gauge/C, clear the whole item/actor area in case of any face changes. --- src/window_battlestatus.cpp | 61 ++++++++++++++++++++++++------------- src/window_battlestatus.h | 8 ++++- 2 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/window_battlestatus.cpp b/src/window_battlestatus.cpp index bd86c3dd07..0ab2ef10b4 100644 --- a/src/window_battlestatus.cpp +++ b/src/window_battlestatus.cpp @@ -82,22 +82,36 @@ void Window_BattleStatus::Refresh() { item_max = std::min(item_max, 4); + bool needs_redraw = false; + // Check if any actor IDs have changed, and if so, redraw the whole window. for (int i = 0; i < item_max; i++) { - // The party only contains valid battlers - const Game_Battler* actor; - if (enemy) { - actor = &(*Main_Data::game_enemyparty)[i]; + const Game_Battler* actor = GetActorForItem(i); + if (itemStates[i].actor_id != actor->GetId()) { + needs_redraw = true; + break; } - else { - actor = &(*Main_Data::game_party)[i]; + } + // Also redraw if we have any rows to remove + for (int i = item_max; i < 4; i++) { + if (itemStates[i].is_drawn) { + needs_redraw = true; + break; } - - // If the actor ID for this item has changed, clear the graphics and reset the cached data - if (itemStates[i].actor_id != actor->GetId()) { - if (!isEmpty) { ClearItemGraphics(i); } + } + if (needs_redraw) { + if (!isEmpty) { + contents->Clear(); + isEmpty = true; + } + for (int i = item_max; i < 4; i++) { itemStates[i].reset(); - itemStates[i].actor_id = actor->GetId(); } + } + + for (int i = 0; i < item_max; i++) { + // The party only contains valid battlers + const Game_Battler* actor = GetActorForItem(i); + itemStates[i].actor_id = actor->GetId(); bool hp_changed = itemStates[i].hp != actor->GetHp(); bool hp_max_changed = itemStates[i].max_hp != actor->GetMaxHp(); @@ -105,11 +119,13 @@ void Window_BattleStatus::Refresh() { bool sp_max_changed = itemStates[i].max_sp != actor->GetMaxSp(); if (lcf::Data::battlecommands.battle_type != lcf::rpg::BattleCommands::BattleType_gauge || enemy) { + + int y = menu_item_height / 8 + i * menu_item_height; const lcf::rpg::State* state = actor->GetSignificantState(); - bool state_changed = (state == NULL && itemStates[i].state_name.compare("") != 0 || itemStates[i].state_color != 0); - state_changed = state_changed || (state != NULL && (itemStates[i].state_name.compare(ToString(state->name)) != 0 || itemStates[i].state_color != state->color)); + bool state_changed = (state == NULL && itemStates[i].state_name.compare("") != 0 || itemStates[i].state_color != 0) + || (state != NULL && (itemStates[i].state_name.compare(ToString(state->name)) != 0 || itemStates[i].state_color != state->color)); int state_x; @@ -164,15 +180,18 @@ void Window_BattleStatus::Refresh() { itemStates[i].is_drawn = true; } - for (int i = item_max; i < 4; i++) { - if (itemStates[i].is_drawn) { - ClearItemGraphics(i); - itemStates[i].reset(); - } - } isEmpty = false; } +const Game_Battler* Window_BattleStatus::GetActorForItem(int i_actor) { + if (enemy) { + return &(*Main_Data::game_enemyparty)[i_actor]; + } + else { + return &(*Main_Data::game_party)[i_actor]; + } +} + void Window_BattleStatus::ClearItemGraphics(int i_item) { if (lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_gauge) { contents->ClearRect(Rect(80 * i_item, 0, 80, height)); @@ -208,9 +227,9 @@ void Window_BattleStatus::RefreshGauge(const Game_Battler* actor, int i_item, bo // is also overlapped by the gauge graphics, so just redraw everything. (also do this if the ATB gauge resets.) // Could be fine-tuned, but performance seems ok for now. if (hp_changed || hp_max_changed || sp_changed || sp_max_changed || face_changed || atb_bar_w < itemStates[i_item].atb_bar_width) { - // Clear number and gauge drawing area + // Clear the entire area for this item, just in case if (!isEmpty) { - contents->ClearRect(Rect(40 + 80 * i_item, actor_face_height, 8 * 4, 48)); + ClearItemGraphics(i_item); } // Note that both clear and redraw are needed because some games don't have diff --git a/src/window_battlestatus.h b/src/window_battlestatus.h index dbf3b28bd0..2aa16ac9a5 100644 --- a/src/window_battlestatus.h +++ b/src/window_battlestatus.h @@ -47,7 +47,8 @@ class Window_BattleStatus : public Window_Selectable { Window_BattleStatus(int ix, int iy, int iwidth, int iheight, bool enemy = false); /** - * Renders the current status on the window. + * Renders the current status on the window. Updates incrementally, based on current party state when called. + * This allows Refresh() to be called every frame without significant CPU load. */ void Refresh(); @@ -76,6 +77,11 @@ class Window_BattleStatus : public Window_Selectable { */ void UpdateCursorRect() override; + /** + * Get the appropriate actor for the index from the player or enemy party. + */ + const Game_Battler* GetActorForItem(int i_actor); + /** * Clear the graphics for one character's row or face portrait area. */ From 84111b79f56c896c74f932b4ca2788d4953b153e Mon Sep 17 00:00:00 2001 From: Vannevar Xbox Date: Sat, 20 Jun 2026 14:53:56 -0400 Subject: [PATCH 3/3] Streamlined Window_Battlestatus caching struct, now only clear full actor face if face redraw is needed, fixed a couple frames for clearing content, removed function ClearItemGraphics which is now unused --- src/window_battlestatus.cpp | 29 ++++++++++++----------------- src/window_battlestatus.h | 5 ----- 2 files changed, 12 insertions(+), 22 deletions(-) diff --git a/src/window_battlestatus.cpp b/src/window_battlestatus.cpp index 0ab2ef10b4..d13033ccac 100644 --- a/src/window_battlestatus.cpp +++ b/src/window_battlestatus.cpp @@ -137,7 +137,7 @@ void Window_BattleStatus::Refresh() { int sp_x = 220 - spdigits * 6; // Just clear and redraw the whole Status/HP/MP area for now when something changes - if (!isEmpty) { contents->ClearRect(Rect(state_x, y, sp_x - state_x, menu_item_height)); } + if (!isEmpty) { contents->ClearRect(Rect(state_x, y, width - state_x, menu_item_height)); } DrawActorState(*actor, state_x, y); DrawActorHp(*actor, 178 - hpdigits * 6 - spdigits * 6, y, hpdigits, true); DrawActorSp(*actor, sp_x, y, spdigits, false); @@ -192,16 +192,6 @@ const Game_Battler* Window_BattleStatus::GetActorForItem(int i_actor) { } } -void Window_BattleStatus::ClearItemGraphics(int i_item) { - if (lcf::Data::battlecommands.battle_type == lcf::rpg::BattleCommands::BattleType_gauge) { - contents->ClearRect(Rect(80 * i_item, 0, 80, height)); - } - else { - contents->ClearRect(Rect(0, menu_item_height / 8 + i_item * menu_item_height, width, menu_item_height)); - } - -} - void Window_BattleStatus::RefreshGauge(const Game_Battler* actor, int i_item, bool hp_changed, bool hp_max_changed, bool sp_changed, bool sp_max_changed) { if (!Feature::HasRpg2k3BattleSystem()) { @@ -227,13 +217,18 @@ void Window_BattleStatus::RefreshGauge(const Game_Battler* actor, int i_item, bo // is also overlapped by the gauge graphics, so just redraw everything. (also do this if the ATB gauge resets.) // Could be fine-tuned, but performance seems ok for now. if (hp_changed || hp_max_changed || sp_changed || sp_max_changed || face_changed || atb_bar_w < itemStates[i_item].atb_bar_width) { - // Clear the entire area for this item, just in case - if (!isEmpty) { - ClearItemGraphics(i_item); + + if (!isEmpty && face_changed) { + // If the face graphic changed, clear the entire available area for this actor item. + contents->ClearRect(Rect(80 * i_item, 0, 80, height)); + } + else if (!isEmpty) { + // Otherwise, just clear the part of the face that could be overlapped by the numbers. + // Note that both clear and redraw are needed because some games don't have + // face graphics that are huge enough to clear the left side of the number area (e.g. Ara Fell) + contents->ClearRect(Rect(40 + 80 * i_item, actor_face_height, 8 * 4, 48)); } - // Note that both clear and redraw are needed because some games don't have - // face graphics that are huge enough to clear the left side of the number area (e.g. Ara Fell) DrawActorFace(*static_cast(actor), 80 * i_item, actor_face_height); int x = x_start; @@ -283,7 +278,7 @@ void Window_BattleStatus::RefreshGauge(const Game_Battler* actor, int i_item, bo // If any of these have changed, then full clear and redraw since there's so much overlap. if (hp_changed || hp_max_changed || sp_changed || sp_max_changed || atb_bar_w < itemStates[i_item].atb_bar_width || has_opaque_gauge != itemStates[i_item].has_opaque_gauge) { if (!isEmpty) { - contents->ClearRect(Rect(192, y, 107, menu_item_height)); + contents->ClearRect(Rect(130, y, width - 130, menu_item_height)); } // RPG_RT Bug (?): Gauge hidden when selected due to transparency (wrong color when rendering) if (has_opaque_gauge) { diff --git a/src/window_battlestatus.h b/src/window_battlestatus.h index 2aa16ac9a5..6602c69505 100644 --- a/src/window_battlestatus.h +++ b/src/window_battlestatus.h @@ -81,11 +81,6 @@ class Window_BattleStatus : public Window_Selectable { * Get the appropriate actor for the index from the player or enemy party. */ const Game_Battler* GetActorForItem(int i_actor); - - /** - * Clear the graphics for one character's row or face portrait area. - */ - void ClearItemGraphics(int i_item); /** * Redraws the characters time gauge, as well as HP and SP if applicable.