Skip to content

Commit

Permalink
Merge pull request calref#536 from NQNStudios:recast-hint
Browse files Browse the repository at this point in the history
Quality of life: Spellcasting

This makes changes to the spellcasting UI.

* M or P to recast will no longer default to Light or Minor Bless/Minor Heal. You need to cast something before recast becomes available. This fixes calref#535 and I think it's disorienting when I've just started the game and M casts Light in a town that's fully lit, so the change is generally good I'd say.
* I implemented a recasting hint in the text bar, which was one of the things I mentioned in my quality-of-life checklist #16. It replaces the status icons in combat mode.
* Sometimes when my eyes glaze over, I think I'm casting the spell on the wrong side of the LED. I thought there was a bug when I cast Long Light instead of Dumbfound (even though I know the distance between the two is pretty large -- I wasn't paying much attention). I thought it would be nice to highlight the name of the selected spell. Light green seemed to make more sense than red for that, because the LED turns green. Then I made the caster/target selection texts also use light green instead of red, to match. Uncastable spells are grey.
  • Loading branch information
CelticMinstrel authored Jan 23, 2025
2 parents c39172b + d8089fe commit 2ec456b
Show file tree
Hide file tree
Showing 13 changed files with 370 additions and 196 deletions.
341 changes: 210 additions & 131 deletions rsrc/dialogs/cast-spell.xml

Large diffs are not rendered by default.

39 changes: 6 additions & 33 deletions src/dialogxml/dialogs/dialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <functional>
#include <sstream>
#include <string>
#include <map>
#include "dialog.hpp"
#include "gfx/tiling.hpp" // for bg
#include "fileio/resmgr/res_dialog.hpp"
Expand Down Expand Up @@ -45,6 +46,8 @@ cDialog* cDialog::topWindow = nullptr;
void (*cDialog::redraw_everything)() = nullptr;
std::mt19937 cDialog::ui_rand;

extern std::map<std::string,sf::Color> colour_map;

extern bool check_for_interrupt(std::string);

std::string cDialog::generateRandomString(){
Expand Down Expand Up @@ -86,39 +89,9 @@ sf::Color cControl::parseColor(string what){
}
}
clr.r = r, clr.g = g, clr.b = b;
}else if(what == "black")
clr.r = 0x00, clr.g = 0x00, clr.b = 0x00;
else if(what == "red")
clr.r = 0xFF, clr.g = 0x00, clr.b = 0x00;
else if(what == "lime")
clr.r = 0x00, clr.g = 0xFF, clr.b = 0x00;
else if(what == "blue")
clr.r = 0x00, clr.g = 0x00, clr.b = 0xFF;
else if(what == "yellow")
clr.r = 0xFF, clr.g = 0xFF, clr.b = 0x00;
else if(what == "aqua")
clr.r = 0x00, clr.g = 0xFF, clr.b = 0xFF;
else if(what == "fuchsia")
clr.r = 0xFF, clr.g = 0x00, clr.b = 0xFF;
else if(what == "white")
clr.r = 0xFF, clr.g = 0xFF, clr.b = 0xFF;
else if(what == "gray" || what == "grey")
clr.r = 0x80, clr.g = 0x80, clr.b = 0x80;
else if(what == "maroon")
clr.r = 0x80, clr.g = 0x00, clr.b = 0x00;
else if(what == "green")
clr.r = 0x00, clr.g = 0x80, clr.b = 0x00;
else if(what == "navy")
clr.r = 0x00, clr.g = 0x00, clr.b = 0x80;
else if(what == "olive")
clr.r = 0x80, clr.g = 0x80, clr.b = 0x00;
else if(what == "teal")
clr.r = 0x00, clr.g = 0x80, clr.b = 0x80;
else if(what == "purple")
clr.r = 0x80, clr.g = 0x00, clr.b = 0x80;
else if(what == "silver")
clr.r = 0xC0, clr.g = 0xC0, clr.b = 0xC0;
else throw -1;
}else if(colour_map.find(what) != colour_map.end()){
return colour_map[what];
}else throw -1;
return clr;
}

Expand Down
10 changes: 10 additions & 0 deletions src/game/boe.actions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,14 @@ void handle_spellcast(eSkill which_type, bool& did_something, bool& need_redraw,
short store_sp[6];
extern short spec_target_fail;
extern eSpecCtxType spec_target_type;
// Dual-caster recast hint toggle:
// Change the recast hint to mage if last spell wasn't mage
if(spell_forced && is_combat() && univ.current_pc().last_cast_type != which_type){
spell_forced = false;
univ.current_pc().last_cast_type = which_type;
need_redraw = true;
return;
}
if(!someone_awake()) {
ASB("Everyone's asleep/paralyzed.");
need_reprint = true;
Expand Down Expand Up @@ -1864,6 +1872,7 @@ void handle_menu_spell(eSpell spell_picked) {
spell_forced = true;
pc_casting = univ.cur_pc;
univ.current_pc().last_cast[spell_type] = spell_picked;
univ.current_pc().last_cast_type = spell_type;
if(spell_type == eSkill::MAGE_SPELLS)
store_mage = spell_picked;
else store_priest = spell_picked;
Expand Down Expand Up @@ -2472,6 +2481,7 @@ bool handle_keystroke(const sf::Event& event, cFramerateLimiter& fps_limiter){
// cast multi-target spell, set # targets to 0 so that space clicked doesn't matter
num_targets_left = 0;
handle_target_space(center, did_something, need_redraw, need_reprint);
advance_time(did_something, need_redraw, need_reprint);
} else if(overall_mode == MODE_SPELL_TARGET)
// Rotate a force wall
spell_cast_hit_return();
Expand Down
48 changes: 43 additions & 5 deletions src/game/boe.graphics.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -611,8 +611,42 @@ void draw_text_bar() {
}
if((is_combat()) && (univ.cur_pc < 6) && !monsters_going) {
std::ostringstream sout;
sout << univ.current_pc().name << " (ap: " << univ.current_pc().ap << ')';
put_text_bar(sout.str());

cPlayer& current_pc = univ.current_pc();
sout << current_pc.name << " (ap: " << current_pc.ap << ')';

// Spellcasters print a hint for recasting.
// There's not enough space to print 2 hints for dual-casters,
// so just handle the last type cast.
eSkill type = current_pc.last_cast_type;
std::string hint_prefix = "";
std::ostringstream hint_out;
switch(type){
case eSkill::MAGE_SPELLS:
hint_prefix = "M";
break;
case eSkill::PRIEST_SPELLS:
hint_prefix = "P";
break;
// The only other expected value is eSkill::INVALID
default:
break;
}
if(!hint_prefix.empty()){
hint_out << hint_prefix << ": ";
if(current_pc.last_cast[type] != eSpell::NONE){
const cSpell& spell = (*current_pc.last_cast[type]);
if(pc_can_cast_spell(current_pc,type) && spell.cost <= current_pc.get_magic()) {
hint_out << "Recast " << spell.name();
}else{
hint_out << "Cannot recast";
}
}else{
hint_out << "No spell to recast";
}
}

put_text_bar(sout.str(), hint_out.str());
}
if((is_combat()) && (monsters_going))
// Print bar for 1st monster with >0 ap - that is monster that is going
Expand All @@ -623,7 +657,7 @@ void draw_text_bar() {
}
}

void put_text_bar(std::string str) {
void put_text_bar(std::string str, std::string right_str) {
text_bar_gworld.setActive(false);
auto& bar_gw = *ResMgr::graphics.get("textbar");
rect_draw_some_item(bar_gw, rectangle(bar_gw), text_bar_gworld, rectangle(bar_gw));
Expand All @@ -635,9 +669,13 @@ void put_text_bar(std::string str) {
rectangle to_rect = rectangle(text_bar_gworld);
to_rect.top += 7;
to_rect.left += 5;
to_rect.right -= 5;
win_draw_string(text_bar_gworld, to_rect, str, eTextMode::LEFT_TOP, style);

if(!monsters_going) {
// the recast hint will replace status icons:
if(!right_str.empty()){
// Style has to be wrap to get right-alignment
win_draw_string(text_bar_gworld, to_rect, right_str, eTextMode::WRAP, style, true);
}else if(!monsters_going) {
sf::Texture& status_gworld = *ResMgr::graphics.get("staticons");
to_rect.top -= 2;
to_rect.left = to_rect.right - 15;
Expand Down
2 changes: 1 addition & 1 deletion src/game/boe.graphics.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ void redraw_screen(int refresh);
void put_background();
void draw_text_bar();
void refresh_text_bar();
void put_text_bar(std::string str);
void put_text_bar(std::string str, std::string right_str = "");
void draw_terrain(short mode = 0);
void place_trim(short q,short r,location where,ter_num_t ter_type);
void draw_trim(short q,short r,short which_trim,short which_mode);
Expand Down
2 changes: 1 addition & 1 deletion src/game/boe.main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ eStatMode stat_screen_mode;
short anim_step = -1;

// Spell casting globals
eSpell store_mage = eSpell::LIGHT, store_priest = eSpell::BLESS_MINOR;
eSpell store_mage = eSpell::NONE, store_priest = eSpell::NONE;
short store_mage_lev = 0, store_priest_lev = 0;
short store_spell_target = 6,pc_casting;
short num_targets_left = 0;
Expand Down
68 changes: 47 additions & 21 deletions src/game/boe.party.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ short combat_percent[20] = {
70,70,67,62,57,52,47,42,40,40};

short who_cast,which_pc_displayed;
// Light can be cast in or out of combat
const eSpell DEFAULT_SELECTED_MAGE = eSpell::LIGHT;
// Bless can only be cast in combat, so separate defaults are needed
const eSpell DEFAULT_SELECTED_PRIEST = eSpell::HEAL_MINOR;
const eSpell DEFAULT_SELECTED_PRIEST_COMBAT = eSpell::BLESS_MINOR;
eSpell town_spell;
extern bool spell_freebie;
extern eSpecCtxType spec_target_type;
Expand Down Expand Up @@ -94,6 +99,9 @@ short spell_index[38] = {38,39,40,41,42,43,44,45,90,90,46,47,48,49,50,51,52,53,9
// Says which buttons hit which spells on second spell page, 90 means no button
bool can_choose_caster;

const sf::Color SELECTED_COLOUR = Colours::LIGHT_GREEN;
const sf::Color DISABLED_COLOUR = Colours::GREY;

// Dialog vars
short store_graphic_pc_num ;
short store_graphic_mode ;
Expand Down Expand Up @@ -490,6 +498,13 @@ bool repeat_cast_ok(eSkill type) {
what_spell = univ.party[who_would_cast].last_cast[type];
else what_spell = type == eSkill::MAGE_SPELLS ? store_mage : store_priest;

if(what_spell == eSpell::NONE){
std::ostringstream sout;
sout << "Repeat cast: No " << (type == eSkill::MAGE_SPELLS ? "mage" : "priest") << " spell stored.";
add_string_to_buf(sout.str());
return false;
}

if(!pc_can_cast_spell(univ.party[who_would_cast],what_spell)) {
add_string_to_buf("Repeat cast: Can't cast.");
return false;
Expand Down Expand Up @@ -1640,8 +1655,8 @@ static void draw_spell_info(cDialog& me, const eSkill store_situation, const sho
}
break;
case SELECT_ANY:
// TODO: Split off party members should probably be excluded too?
if(univ.party[i].main_status != eMainStatus::ABSENT) {
// Absent party members and split-off party members are excluded
if(univ.party[i].main_status != eMainStatus::ABSENT && univ.party[i].main_status < eMainStatus::SPLIT) {
me[id].show();
}
else {
Expand Down Expand Up @@ -1691,6 +1706,7 @@ static void draw_spell_pc_info(cDialog& me) {
if(univ.party[i].main_status != eMainStatus::ABSENT) {
me["pc" + n].setText(univ.party[i].name);

me["arrow" + n].hide();
if(univ.party[i].main_status == eMainStatus::ALIVE) {
me["hp" + n].setTextToNum(univ.party[i].cur_health);
me["sp" + n].setTextToNum(univ.party[i].cur_sp);
Expand All @@ -1706,7 +1722,7 @@ static void put_pc_caster_buttons(cDialog& me) {
std::string n = boost::lexical_cast<std::string>(i + 1);
if(me["caster" + n].isVisible()) {
if(i == pc_casting)
me["pc" + n].setColour(Colours::RED);
me["pc" + n].setColour(SELECTED_COLOUR);
else me["pc" + n].setColour(me.getDefTextClr());
}
}
Expand All @@ -1716,13 +1732,11 @@ static void put_pc_target_buttons(cDialog& me, short& store_last_target_darkened

if(store_spell_target < 6) {
std::string n = boost::lexical_cast<std::string>(store_spell_target + 1);
me["hp" + n].setColour(Colours::RED);
me["sp" + n].setColour(Colours::RED);
me["arrow" + n].show();
}
if((store_last_target_darkened < 6) && (store_last_target_darkened != store_spell_target)) {
std::string n = boost::lexical_cast<std::string>(store_last_target_darkened + 1);
me["hp" + n].setColour(me.getDefTextClr());
me["sp" + n].setColour(me.getDefTextClr());
me["arrow" + n].hide();
}
store_last_target_darkened = store_spell_target;
}
Expand All @@ -1740,12 +1754,16 @@ static void put_spell_led_buttons(cDialog& me, const eSkill store_situation,cons
eSpell spell = cSpell::fromNum(store_situation, spell_for_this_button);
if(store_spell == spell_for_this_button) {
led.setState(led_green);
// Text color:
led.setColour(SELECTED_COLOUR);
}
else if(pc_can_cast_spell(univ.party[pc_casting],spell)) {
led.setState(led_red);
led.setColour(me.getDefTextClr());
}
else {
led.setState(led_off);
led.setColour(DISABLED_COLOUR);
}
}
}
Expand Down Expand Up @@ -1862,9 +1880,7 @@ static bool pick_spell_select_led(cDialog& me, std::string id, eKeyMod mods, con
me["feedback"].setText(bad_spell);
}
else {
if(store_situation == eSkill::MAGE_SPELLS)
store_spell = (on_which_spell_page == 0) ? item_hit : spell_index[item_hit];
else store_spell = (on_which_spell_page == 0) ? item_hit : spell_index[item_hit];
store_spell = (on_which_spell_page == 0) ? item_hit : spell_index[item_hit];
draw_spell_info(me, store_situation, store_spell);
put_spell_led_buttons(me, store_situation, store_spell);

Expand Down Expand Up @@ -1914,13 +1930,15 @@ static bool finish_pick_spell(cDialog& me, bool spell_toast, const short store_s
if(store_situation == eSkill::MAGE_SPELLS && (*picked_spell).need_select == SELECT_NO) {
store_last_cast_mage = pc_casting;
univ.party[pc_casting].last_cast[store_situation] = picked_spell;
univ.party[pc_casting].last_cast_type = store_situation;
me.toast(false);
me.setResult<short>(store_spell);
return true;
}
if(store_situation == eSkill::PRIEST_SPELLS && (*picked_spell).need_select == SELECT_NO) {
store_last_cast_priest = pc_casting;
univ.party[pc_casting].last_cast[store_situation] = picked_spell;
univ.party[pc_casting].last_cast_type = store_situation;
me.toast(false);
me.setResult<short>(store_spell);
return true;
Expand All @@ -1938,6 +1956,7 @@ static bool finish_pick_spell(cDialog& me, bool spell_toast, const short store_s
store_last_cast_mage = pc_casting;
else store_last_cast_priest = pc_casting;
univ.party[pc_casting].last_cast[store_situation] = picked_spell;
univ.party[pc_casting].last_cast_type = store_situation;
me.toast(true);
return true;
}
Expand All @@ -1947,7 +1966,7 @@ static bool finish_pick_spell(cDialog& me, bool spell_toast, const short store_s
//short situation; // 0 - out 1 - town 2 - combat
eSpell pick_spell(short pc_num,eSkill type) { // 70 - no spell OW spell num
using namespace std::placeholders;
eSpell store_spell = type == eSkill::MAGE_SPELLS ? store_mage : store_priest;
eSpell default_spell = type == eSkill::MAGE_SPELLS ? store_mage : store_priest;
short former_target = store_spell_target;
short dark = 6;

Expand Down Expand Up @@ -1995,29 +2014,38 @@ eSpell pick_spell(short pc_num,eSkill type) { // 70 - no spell OW spell num
// If in combat, make the spell being cast this PCs most recent spell
if(is_combat()) {
if(type == eSkill::MAGE_SPELLS)
store_spell = univ.party[pc_casting].last_cast[eSkill::MAGE_SPELLS];
else store_spell = univ.party[pc_casting].last_cast[eSkill::PRIEST_SPELLS];
default_spell = univ.party[pc_casting].last_cast[eSkill::MAGE_SPELLS];
else{
default_spell = univ.party[pc_casting].last_cast[eSkill::PRIEST_SPELLS];
if(default_spell == eSpell::NONE){
default_spell = DEFAULT_SELECTED_PRIEST_COMBAT;
}
}
}

if(default_spell == eSpell::NONE){
default_spell = type == eSkill::MAGE_SPELLS ? DEFAULT_SELECTED_MAGE : DEFAULT_SELECTED_PRIEST;
}

// Keep the stored spell, if it's still castable
if(!pc_can_cast_spell(univ.party[pc_casting],store_spell)) {
if(!pc_can_cast_spell(univ.party[pc_casting],default_spell)) {
if(type == eSkill::MAGE_SPELLS) {
store_spell = eSpell::LIGHT;
default_spell = DEFAULT_SELECTED_MAGE;
}
else {
store_spell = eSpell::HEAL_MINOR;
default_spell = DEFAULT_SELECTED_PRIEST;
}
}

// If a target is needed, keep the same target if that PC still targetable
if(store_spell_target < 6) {
if((*store_spell).need_select != SELECT_NO) {
if((*default_spell).need_select != SELECT_NO) {
if(univ.party[store_spell_target].main_status != eMainStatus::ALIVE)
store_spell_target = 6;
} else store_spell_target = 6;
}

short former_spell = int(store_spell) % 100;
short former_spell = int(default_spell) % 100;
// Set the spell page, based on starting spell
if(former_spell >= 38) on_which_spell_page = 1;
else on_which_spell_page = 0;
Expand All @@ -2042,9 +2070,7 @@ eSpell pick_spell(short pc_num,eSkill type) { // 70 - no spell OW spell num
cLed& led = dynamic_cast<cLed&>(castSpell[id]);
led.attachKey(key);
castSpell.addLabelFor(id, {static_cast<char>(i > 25 ? toupper(key.c) : key.c)}, LABEL_LEFT, 8, true);
if(spell_index[i] == 90){
continue;
}
// All LEDs should get the click handler and set state, because page 2 will hide them if necessary
led.setState((pc_can_cast_spell(univ.party[pc_casting],cSpell::fromNum(type,on_which_spell_page == 0 ? i : spell_index[i])))
? led_red : led_green);
led.attachClickHandler(std::bind(pick_spell_select_led, _1, _2, _3, type, std::ref(dark), std::ref(former_spell)));
Expand Down
Loading

0 comments on commit 2ec456b

Please sign in to comment.