Files
oboe/src/game/boe.menu.cpp

345 lines
12 KiB
C++

// Author: xq, Saturday 2020-01-25
#include "boe.menu.hpp"
#include "boe.menus.hpp"
#include "boe.party.hpp"
#include "boe.graphics.hpp"
#include <sstream>
#include <stdexcept>
#include <utility>
OpenBoEMenu::OpenBoEMenu(cUniverse& universe)
: tgui { mainPtr() }
, univ { universe } {
// Build a menubar and store it in tgui with a known name
this->tgui.add(this->build_menubar(), this->internal_menubar_widget_name);
// Space for 2x "About this menu" connections + 1 for every possible spell
this->spell_menus_connection_ids.reserve(2 + NUM_MAGE_SPELLS + NUM_PRIEST_SPELLS);
// These menus are dynamic so we need to generate them
this->update_spell_menus();
}
tgui::MenuBar::Ptr OpenBoEMenu::build_menubar() const {
auto menubar = tgui::MenuBar::create();
menubar->setSize("100%", MENUBAR_HEIGHT);
this->add_menu_placeholders(menubar);
this->add_persistent_menu_items(menubar);
return menubar;
}
// This method ensures that the menus on the menubar are in specific order
void OpenBoEMenu::add_menu_placeholders(tgui::MenuBar::Ptr& menubar) const {
menubar->addMenu("File");
menubar->addMenu("Options");
menubar->addMenu("Actions");
menubar->addMenu("Monsters");
menubar->addMenu("Cast Mage");
menubar->addMenu("Cast Priest");
menubar->addMenu("Library");
menubar->addMenu("Help");
}
// This method fills the menu with items that never change.
void OpenBoEMenu::add_persistent_menu_items(tgui::MenuBar::Ptr& menubar) const {
const std::vector<std::pair <OpenBoEMenu::MenuHierarchy, eMenu>> persistent_menu_items {
{ { "File", "New Game Ctrl-N" }, eMenu::FILE_NEW },
{ { "File", "Open Game Ctrl-O" }, eMenu::FILE_OPEN },
{ { "File", "Abort" }, eMenu::FILE_ABORT },
{ { "File", "Save Game Ctrl-S" }, eMenu::FILE_SAVE },
{ { "File", "Save As... Ctrl-Shift-S" }, eMenu::FILE_SAVE_AS },
{ { "File", "Preferences" }, eMenu::PREFS },
{ { "File", "Quit Ctrl-Q" }, eMenu::QUIT },
{ { "Options", "Pick New PC Graphic" }, eMenu::OPTIONS_PC_GRAPHIC },
{ { "Options", "Pick New PC Name" }, eMenu::OPTIONS_RENAME_PC },
{ { "Options", "Create New PC" }, eMenu::OPTIONS_NEW_PC },
{ { "Options", "Delete PC" }, eMenu::OPTIONS_DELETE_PC },
{ { "Options", "See Talking Notes" }, eMenu::OPTIONS_TALK_NOTES },
{ { "Options", "See Encounter Notes" }, eMenu::OPTIONS_ENCOUNTER_NOTES },
{ { "Options", "See Overall Party Stats" }, eMenu::OPTIONS_STATS },
{ { "Options", "See Journal" }, eMenu::OPTIONS_JOURNAL },
{ { "Actions", "Do Alchemy Ctrl-A" }, eMenu::ACTIONS_ALCHEMY },
{ { "Actions", "Wait 80 Moves Ctrl-W" }, eMenu::ACTIONS_WAIT },
{ { "Actions", "Display AutoMap A" }, eMenu::ACTIONS_AUTOMAP },
{ { "Library", "Mage Spells" }, eMenu::LIBRARY_MAGE },
{ { "Library", "Priest Spells" }, eMenu::LIBRARY_PRIEST },
{ { "Library", "Skill Info" }, eMenu::LIBRARY_SKILLS },
{ { "Library", "Alchemy/Poison" }, eMenu::LIBRARY_ALCHEMY },
{ { "Library", "Tip of the Day" }, eMenu::LIBRARY_TIPS },
{ { "Library", "Show Introductory Dialog" }, eMenu::LIBRARY_INTRO },
{ { "Help", "Index F1" }, eMenu::HELP_TOC },
{ { "Help", "About Blades of Exile" }, eMenu::ABOUT },
{ { "Help", "Outdoor Commands" }, eMenu::HELP_OUT },
{ { "Help", "Town Commands" }, eMenu::HELP_TOWN },
{ { "Help", "Combat Commands" }, eMenu::HELP_COMBAT },
{ { "Help", "Magic Barrier Help" }, eMenu::HELP_BARRIER },
{ { "Help", "Hints, Bugs and Comments" }, eMenu::HELP_HINTS },
{ { "Help", "Spell Casting Help" }, eMenu::HELP_SPELLS },
};
// Note that signal connection ids are discarded.
for(const auto& item : persistent_menu_items) {
menubar->addMenuItem(item.first);
menubar->connectMenuItem(item.first, handle_menu_choice, item.second);
}
}
bool OpenBoEMenu::handle_event(const sf::Event& event) {
if(event.type == sf::Event::KeyPressed && this->handle_keypressed_event(event))
return true;
return this->tgui.handleEvent(event);
}
// Returns true if event was consumed
bool OpenBoEMenu::handle_keypressed_event(const sf::Event& event) const {
// NOTE: since we are manually adding keyboard shortcut descriptions
// to the menu items, they become parts of menu hierarchies
bool event_was_consumed { false };
if(this->is_control_key_pressed()) {
if(this->is_shift_key_pressed()) {
event_was_consumed = this->handle_ctrl_shift_keypress(event);
} else {
event_was_consumed = this->handle_ctrl_keypress(event);
}
}
switch(event.key.code) {
case sf::Keyboard::F1:
handle_menu_choice(eMenu::HELP_TOC);
event_was_consumed = true;
break;
default: break;
}
return event_was_consumed;
}
bool OpenBoEMenu::handle_ctrl_keypress(const sf::Event& event) const {
auto menubar = this->get_menubar_ptr();
bool event_was_consumed { false };
switch(event.key.code) {
case sf::Keyboard::S:
if(!menubar->getMenuItemEnabled({ "File", "Save Game Ctrl-S" })) break;
handle_menu_choice(eMenu::FILE_SAVE);
event_was_consumed = true;
break;
case sf::Keyboard::A:
if(!menubar->getMenuItemEnabled({ "Actions", "Do Alchemy Ctrl-A" })) break;
handle_menu_choice(eMenu::ACTIONS_ALCHEMY);
event_was_consumed = true;
break;
case sf::Keyboard::W:
if(!menubar->getMenuItemEnabled({ "Actions", "Wait 80 Moves Ctrl-W" })) break;
handle_menu_choice(eMenu::ACTIONS_WAIT);
event_was_consumed = true;
break;
case sf::Keyboard::N:
handle_menu_choice(eMenu::FILE_NEW);
event_was_consumed = true;
break;
case sf::Keyboard::O:
handle_menu_choice(eMenu::FILE_OPEN);
event_was_consumed = true;
break;
case sf::Keyboard::Q:
handle_menu_choice(eMenu::QUIT);
event_was_consumed = true;
break;
default: break;
}
return event_was_consumed;
}
bool OpenBoEMenu::handle_ctrl_shift_keypress(const sf::Event& event) const {
auto menubar = this->get_menubar_ptr();
bool event_was_consumed { false };
switch(event.key.code) {
case sf::Keyboard::S:
if(!menubar->getMenuItemEnabled({ "File", "Save As... Ctrl-Shift-S" })) break;
handle_menu_choice(eMenu::FILE_SAVE_AS);
event_was_consumed = true;
break;
default: break;
}
return event_was_consumed;
}
bool OpenBoEMenu::is_control_key_pressed() const {
// NOTE: Control is not cross-platform (apple)
return (sf::Keyboard::isKeyPressed(sf::Keyboard::LControl)
|| sf::Keyboard::isKeyPressed(sf::Keyboard::RControl));
}
bool OpenBoEMenu::is_shift_key_pressed() const {
return (sf::Keyboard::isKeyPressed(sf::Keyboard::LShift)
|| sf::Keyboard::isKeyPressed(sf::Keyboard::RShift));
}
tgui::MenuBar::Ptr OpenBoEMenu::get_menubar_ptr() const {
return this->tgui.get<tgui::MenuBar>(this->internal_menubar_widget_name);
}
void OpenBoEMenu::draw() {
this->tgui.draw();
}
// This method enables/disables menus based on current game state
// (logic follows the example in windows menu implementation).
void OpenBoEMenu::update_for_game_state(eGameMode overall_mode, bool party_in_memory) {
auto menubar = this->get_menubar_ptr();
if(overall_mode == MODE_STARTUP) {
menubar->setMenuEnabled("Options", false);
menubar->setMenuEnabled("Actions", false);
menubar->setMenuEnabled("Monsters", false);
menubar->setMenuEnabled("Cast Mage", false);
menubar->setMenuEnabled("Cast Priest", false);
menubar->setMenuItemEnabled({ "File", "Save Game Ctrl-S" }, false);
menubar->setMenuItemEnabled({ "File", "Save As... Ctrl-Shift-S" }, party_in_memory);
menubar->setMenuItemEnabled({ "File", "Abort" }, false);
} else {
menubar->setMenuEnabled("Options", true);
menubar->setMenuEnabled("Actions", true);
menubar->setMenuEnabled("Monsters", true);
menubar->setMenuEnabled("Cast Mage", true);
menubar->setMenuEnabled("Cast Priest", true);
menubar->setMenuItemEnabled({ "File", "Save Game Ctrl-S" }, true);
menubar->setMenuItemEnabled({ "File", "Save As... Ctrl-Shift-S" }, true);
menubar->setMenuItemEnabled({ "File", "Abort" }, true);
}
}
// Disconnect all spell menu items from signals and clear the menus
void OpenBoEMenu::purge_spell_menus(tgui::MenuBar::Ptr& menubar) {
for(const auto& connection_id : this->spell_menus_connection_ids) {
if(!menubar->onMenuItemClick.disconnect(connection_id))
throw std::runtime_error { "BUG: attempted to disconnect menubar signal using invalid connection id" };
}
this->spell_menus_connection_ids.clear();
menubar->removeMenuItems("Cast Mage");
menubar->removeMenuItems("Cast Priest");
}
void OpenBoEMenu::update_mage_spells_menu(tgui::MenuBar::Ptr& menubar) {
// Add "About" menu item and store connection id
const OpenBoEMenu::MenuHierarchy about_hierarchy { "Cast Mage", "About this menu" };
menubar->addMenuItem(about_hierarchy);
this->spell_menus_connection_ids.push_back(
menubar->connectMenuItem(about_hierarchy, handle_menu_choice, eMenu::ABOUT_MAGE)
);
// Add every castable mage spell and store connection ids
for(int spell_id = 0; spell_id < NUM_MAGE_SPELLS; ++spell_id) {
eSpell spell = cSpell::fromNum(eSkill::MAGE_SPELLS, spell_id);
if(pc_can_cast_spell(this->univ.current_pc(), spell)) {
const auto spell_hierarchy = this->menu_hierarchy_from_spell(*spell);
menubar->addMenuItem(spell_hierarchy);
// Connect and store connection id
this->spell_menus_connection_ids.push_back(
menubar->connectMenuItem(spell_hierarchy, handle_menu_spell, spell)
);
}
}
}
void OpenBoEMenu::update_priest_spells_menu(tgui::MenuBar::Ptr& menubar) {
// Add "About" menu item and store connection id
const OpenBoEMenu::MenuHierarchy about_hierarchy { "Cast Priest", "About this menu" };
menubar->addMenuItem(about_hierarchy);
this->spell_menus_connection_ids.push_back(
menubar->connectMenuItem(about_hierarchy, handle_menu_choice, eMenu::ABOUT_PRIEST)
);
// Add every castable Priest spell and store connection ids
for (int spell_id = 0; spell_id < NUM_PRIEST_SPELLS; ++spell_id) {
eSpell spell = cSpell::fromNum(eSkill::PRIEST_SPELLS, spell_id);
if(pc_can_cast_spell(this->univ.current_pc(), spell)) {
const auto spell_hierarchy = this->menu_hierarchy_from_spell(*spell);
menubar->addMenuItem(spell_hierarchy);
// Connect and store connection id
this->spell_menus_connection_ids.push_back(
menubar->connectMenuItem(spell_hierarchy, handle_menu_spell, spell)
);
}
}
}
// Create a menu hierarchy from cSpell
OpenBoEMenu::MenuHierarchy OpenBoEMenu::menu_hierarchy_from_spell(const cSpell& spell) const {
OpenBoEMenu::MenuHierarchy hier;
// Menu name, level, name+cost
hier.reserve(3);
// Fill menu name
if(spell.type == eSkill::MAGE_SPELLS) {
hier.push_back("Cast Mage");
} else if(spell.type == eSkill::PRIEST_SPELLS) {
hier.push_back("Cast Priest");
} else {
throw std::runtime_error { "BUG: attempt to build menu hierarchy from unknown spell type" };
}
// Fill level
std::ostringstream level_label;
level_label << "Level " << spell.level;
hier.push_back(level_label.str());
// Fill name+cost
std::ostringstream name_cost_label;
name_cost_label << spell.name() << ", C ";
if(spell.cost >= 0) {
name_cost_label << spell.cost;
} else {
name_cost_label << '?';
}
hier.push_back(name_cost_label.str());
return hier;
}
void OpenBoEMenu::update_spell_menus() {
auto menubar = this->get_menubar_ptr();
this->purge_spell_menus(menubar);
this->update_mage_spells_menu(menubar);
this->update_priest_spells_menu(menubar);
}
void OpenBoEMenu::update_monsters_menu() {
// TODO not implemented.
// How do you fit a dropdown with 256 monster names in a 605*430 window?
//~ { { "Monsters", "About this menu"}, eMenu::ABOUT_MONSTERS },
}