diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst index 67bba635..fc720c22 100644 --- a/docsrc/manual/project-files.rst +++ b/docsrc/manual/project-files.rst @@ -122,6 +122,7 @@ In addition to these files, there are some specific symbol and macro names that ``define_map_section_prefix``, ``MAPSEC_``, expected prefix for location macro names ``define_map_section_empty``, ``NONE``, macro name after prefix for empty region map sections ``define_species_prefix``, ``SPECIES_``, expected prefix for species macro names + ``define_species_empty``, ``NONE``, macro name after prefix for the default species ``regex_behaviors``, ``\bMB_``, regex to find metatile behavior macro names ``regex_obj_event_gfx``, ``\bOBJ_EVENT_GFX_``, regex to find Object Event graphics ID macro names ``regex_items``, ``\bITEM_(?!(B_)?USE_)``, regex to find item macro names diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui index 39f59836..dfea9752 100644 --- a/forms/mainwindow.ui +++ b/forms/mainwindow.ui @@ -1740,7 +1740,7 @@ 0 0 100 - 30 + 16 @@ -1834,7 +1834,7 @@ 0 0 100 - 30 + 16 @@ -1928,7 +1928,7 @@ 0 0 100 - 30 + 16 @@ -2028,7 +2028,7 @@ 0 0 100 - 30 + 16 @@ -2122,7 +2122,7 @@ 0 0 100 - 30 + 16 @@ -2700,8 +2700,8 @@ 0 0 - 100 - 30 + 204 + 16 @@ -2845,6 +2845,17 @@ + + + + ... + + + + :/icons/magnifier.ico:/icons/magnifier.ico + + + diff --git a/forms/wildmonsearch.ui b/forms/wildmonsearch.ui new file mode 100644 index 00000000..117f7242 --- /dev/null +++ b/forms/wildmonsearch.ui @@ -0,0 +1,94 @@ + + + WildMonSearch + + + + 0 + 0 + 547 + 329 + + + + Wild Pokémon Search + + + + + + QFrame::Shape::NoFrame + + + QFrame::Shadow::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + + + :/images/pokemon_icon_placeholder.png + + + Qt::AlignmentFlag::AlignCenter + + + + + + + true + + + QComboBox::InsertPolicy::NoInsert + + + + + + + + + + 4 + + + + + + + + + + + + NoScrollComboBox + QComboBox +
noscrollcombobox.h
+
+
+ + + + +
diff --git a/include/config.h b/include/config.h index 926e93c6..502ee1ec 100644 --- a/include/config.h +++ b/include/config.h @@ -221,6 +221,7 @@ enum ProjectIdentifier { define_map_section_prefix, define_map_section_empty, define_species_prefix, + define_species_empty, regex_behaviors, regex_obj_event_gfx, regex_items, diff --git a/include/core/wildmoninfo.h b/include/core/wildmoninfo.h index d0aa29dc..3c94fb17 100644 --- a/include/core/wildmoninfo.h +++ b/include/core/wildmoninfo.h @@ -5,10 +5,14 @@ #include #include "orderedmap.h" -struct WildPokemon { - int minLevel = 5; - int maxLevel = 5; - QString species = "SPECIES_NONE"; // TODO: Use define_species_prefix +class WildPokemon { +public: + WildPokemon(); + WildPokemon(int minLevel, int maxLevel, const QString &species); + + int minLevel; + int maxLevel; + QString species; }; struct WildMonInfo { @@ -30,7 +34,8 @@ struct EncounterField { typedef QVector EncounterFields; void setDefaultEncounterRate(QString fieldName, int rate); -WildMonInfo getDefaultMonInfo(EncounterField field); +WildMonInfo getDefaultMonInfo(const EncounterField &field); +QVector getWildEncounterPercentages(const EncounterField &field); void combineEncounters(WildMonInfo &to, WildMonInfo from); #endif // GUARD_WILDMONINFO_H diff --git a/include/mainwindow.h b/include/mainwindow.h index 1ee612bc..cf37a298 100644 --- a/include/mainwindow.h +++ b/include/mainwindow.h @@ -28,6 +28,7 @@ #include "gridsettings.h" #include "customscriptseditor.h" #include "wildmonchart.h" +#include "wildmonsearch.h" #include "updatepromoter.h" #include "aboutporymap.h" #include "mapheaderform.h" @@ -282,6 +283,7 @@ private slots: void on_pushButton_DeleteWildMonGroup_clicked(); void on_pushButton_SummaryChart_clicked(); void on_pushButton_ConfigureEncountersJSON_clicked(); + void on_toolButton_WildMonSearch_clicked(); void on_pushButton_CreatePrefab_clicked(); void on_spinBox_SelectedElevation_valueChanged(int elevation); void on_spinBox_SelectedCollision_valueChanged(int collision); @@ -294,6 +296,7 @@ private slots: void reloadScriptEngine(); void on_actionShow_Grid_triggered(); void on_actionGrid_Settings_triggered(); + void openWildMonTable(const QString &mapName, const QString &groupName, const QString &fieldName); public: Ui::MainWindow *ui; @@ -321,6 +324,7 @@ private: QPointer networkAccessManager = nullptr; QPointer aboutWindow = nullptr; QPointer wildMonChart = nullptr; + QPointer wildMonSearch = nullptr; QAction *undoAction = nullptr; QAction *redoAction = nullptr; diff --git a/include/project.h b/include/project.h index 9fb4e2ed..aff38f03 100644 --- a/include/project.h +++ b/include/project.h @@ -152,6 +152,7 @@ public: QVector extraEncounterGroups; bool readSpeciesIconPaths(); + QPixmap getSpeciesIcon(const QString &species) const; QMap speciesToIconPath; void addNewMapsec(const QString &idName); @@ -238,6 +239,7 @@ public: static QString getEmptyMapDefineName(); static QString getDynamicMapDefineName(); static QString getDynamicMapName(); + static QString getEmptySpeciesName(); static int getNumTilesPrimary(); static int getNumTilesTotal(); static int getNumMetatilesPrimary(); diff --git a/include/ui/encountertablemodel.h b/include/ui/encountertablemodel.h index b2a39ad8..6fe54a91 100644 --- a/include/ui/encountertablemodel.h +++ b/include/ui/encountertablemodel.h @@ -14,7 +14,7 @@ class EncounterTableModel : public QAbstractTableModel { Q_OBJECT public: - EncounterTableModel(WildMonInfo monInfo, EncounterFields allFields, int fieldIndex, QObject *parent = nullptr); + EncounterTableModel(const WildMonInfo &monInfo, const EncounterField &field, QObject *parent = nullptr); int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; @@ -28,22 +28,17 @@ public: Slot, Group, Species, MinLevel, MaxLevel, EncounterChance, SlotRatio, EncounterRate, Count }; - WildMonInfo encounterData() const { return this->monInfo; } - EncounterField encounterField() const { return this->encounterFields.at(this->fieldIndex); } - QList percentages() const { return this->slotPercentages; } - void resize(int rows, int cols); + WildMonInfo encounterData() const { return m_monInfo; } + EncounterField encounterField() const { return m_encounterField; } + QList percentages() const { return m_slotPercentages; } private: - WildMonInfo monInfo; - EncounterFields encounterFields; - int fieldIndex; - - int numRows = 0; - int numCols = 0; - - QVector slotRatios; - QList groupNames; - QList slotPercentages; + int m_numRows = 0; + int m_numCols = 0; + WildMonInfo m_monInfo; + EncounterField m_encounterField; + QMap m_groupNames; + QList m_slotPercentages; signals: void edited(); diff --git a/include/ui/montabwidget.h b/include/ui/montabwidget.h index 6d916068..8292d28e 100644 --- a/include/ui/montabwidget.h +++ b/include/ui/montabwidget.h @@ -27,6 +27,8 @@ public: void copy(int index); void paste(int index); + void setCurrentField(const QString &fieldName); + public slots: void setTabActive(int index, bool active = true); void deactivateTab(int tabIndex); @@ -38,6 +40,7 @@ private: QVector activeTabs; QVector addDeleteTabButtons; QVector copyTabButtons; + QMap fieldNameToIndex; Editor *editor; }; diff --git a/include/ui/wildmonsearch.h b/include/ui/wildmonsearch.h new file mode 100644 index 00000000..49e04b57 --- /dev/null +++ b/include/ui/wildmonsearch.h @@ -0,0 +1,47 @@ +#ifndef WILDMONSEARCH_H +#define WILDMONSEARCH_H + +#include + +class Project; + +namespace Ui { +class WildMonSearch; +} + +class WildMonSearch : public QDialog +{ + Q_OBJECT + +public: + explicit WildMonSearch(Project *project, QWidget *parent = nullptr); + ~WildMonSearch(); + + void refresh(); + +signals: + void openWildMonTableRequested(const QString &mapName, const QString &groupName, const QString &fieldName); + +private: + struct RowData { + QString mapName; + QString groupName; + QString fieldName; + QString levelRange; + QString chance; + }; + + Ui::WildMonSearch *ui; + Project *const project; + QMap> percentageStrings; + QMap> resultsCache; + + void addTableEntry(const RowData &rowData); + QList search(const QString &species) const; + void updatePercentageStrings(); + void updateResults(const QString &species); + void cellDoubleClicked(int row, int column); + +}; + +#endif // WILDMONSEARCH_H diff --git a/porymap.pro b/porymap.pro index 8987b5cc..b6758c2e 100644 --- a/porymap.pro +++ b/porymap.pro @@ -139,7 +139,8 @@ SOURCES += src/core/advancemapparser.cpp \ src/log.cpp \ src/ui/uintspinbox.cpp \ src/ui/updatepromoter.cpp \ - src/ui/wildmonchart.cpp + src/ui/wildmonchart.cpp \ + src/ui/wildmonsearch.cpp HEADERS += include/core/advancemapparser.h \ include/core/block.h \ @@ -251,7 +252,8 @@ HEADERS += include/core/advancemapparser.h \ include/log.h \ include/ui/uintspinbox.h \ include/ui/updatepromoter.h \ - include/ui/wildmonchart.h + include/ui/wildmonchart.h \ + include/ui/wildmonsearch.h FORMS += forms/mainwindow.ui \ forms/colorinputwidget.ui \ @@ -281,7 +283,8 @@ FORMS += forms/mainwindow.ui \ forms/customscriptseditor.ui \ forms/customscriptslistitem.ui \ forms/updatepromoter.ui \ - forms/wildmonchart.ui + forms/wildmonchart.ui \ + forms/wildmonsearch.ui RESOURCES += \ resources/images.qrc \ diff --git a/resources/icons/magnifier.ico b/resources/icons/magnifier.ico new file mode 100755 index 00000000..9cbc3618 Binary files /dev/null and b/resources/icons/magnifier.ico differ diff --git a/resources/images.qrc b/resources/images.qrc index 888c5d9d..fc1ab757 100644 --- a/resources/images.qrc +++ b/resources/images.qrc @@ -25,6 +25,7 @@ icons/help.ico icons/link_broken.ico icons/link.ico + icons/magnifier.ico icons/map_edited.ico icons/map_opened.ico icons/map.ico diff --git a/src/config.cpp b/src/config.cpp index ce78d8b9..2356f7a9 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -113,6 +113,7 @@ const QMap> ProjectConfig::defaultIde {ProjectIdentifier::define_map_section_prefix, {"define_map_section_prefix", "MAPSEC_"}}, {ProjectIdentifier::define_map_section_empty, {"define_map_section_empty", "NONE"}}, {ProjectIdentifier::define_species_prefix, {"define_species_prefix", "SPECIES_"}}, + {ProjectIdentifier::define_species_empty, {"define_species_empty", "NONE"}}, // Regex {ProjectIdentifier::regex_behaviors, {"regex_behaviors", "\\bMB_"}}, {ProjectIdentifier::regex_obj_event_gfx, {"regex_obj_event_gfx", "\\bOBJ_EVENT_GFX_"}}, diff --git a/src/core/wildmoninfo.cpp b/src/core/wildmoninfo.cpp index ecad908f..5222b9e6 100644 --- a/src/core/wildmoninfo.cpp +++ b/src/core/wildmoninfo.cpp @@ -1,12 +1,22 @@ #include "wildmoninfo.h" #include "montabwidget.h" +#include "project.h" + +WildPokemon::WildPokemon(int minLevel, int maxLevel, const QString &species) + : minLevel(minLevel), + maxLevel(maxLevel), + species(species) +{} + +WildPokemon::WildPokemon() : WildPokemon(5, 5, Project::getEmptySpeciesName()) +{} QMap defaultEncounterRates; void setDefaultEncounterRate(QString fieldName, int rate) { defaultEncounterRates[fieldName] = rate; } -WildMonInfo getDefaultMonInfo(EncounterField field) { +WildMonInfo getDefaultMonInfo(const EncounterField &field) { WildMonInfo newInfo; newInfo.active = true; newInfo.encounterRate = defaultEncounterRates.value(field.name, 1); @@ -18,6 +28,38 @@ WildMonInfo getDefaultMonInfo(EncounterField field) { return newInfo; } +QVector getWildEncounterPercentages(const EncounterField &field) { + QVector percentages(field.encounterRates.size(), 0); + + if (!field.groups.empty()) { + // This encounter field is broken up into groups (e.g. for fishing rod types). + // Each group's percentages will be relative to the group total, not the overall total. + for (auto groupKeyPair : field.groups) { + int groupTotal = 0; + for (int slot : groupKeyPair.second) { + groupTotal += field.encounterRates.value(slot, 0); + } + if (groupTotal != 0) { + for (int slot : groupKeyPair.second) { + percentages[slot] = static_cast(field.encounterRates.value(slot, 0)) / static_cast(groupTotal); + } + } + } + } else { + // This encounter field has a single group, percentages are relative to the overall total. + int groupTotal = 0; + for (int chance : field.encounterRates) { + groupTotal += chance; + } + if (groupTotal != 0) { + for (int slot = 0; slot < percentages.count(); slot++) { + percentages[slot] = static_cast(field.encounterRates.value(slot, 0)) / static_cast(groupTotal); + } + } + } + return percentages; +} + void combineEncounters(WildMonInfo &to, WildMonInfo from) { to.encounterRate = from.encounterRate; diff --git a/src/editor.cpp b/src/editor.cpp index c22068d2..0b3350e3 100644 --- a/src/editor.cpp +++ b/src/editor.cpp @@ -302,7 +302,7 @@ void Editor::addNewWildMonGroup(QWidget *window) { form.addRow(new QLabel("Group Base Label:"), lineEdit); lineEdit->setValidator(new IdentifierValidator(lineEdit)); connect(lineEdit, &QLineEdit::textChanged, [this, &lineEdit, &buttonBox](QString text){ - if (this->project->encounterGroupLabels.contains(text)) { + if (!this->project->isIdentifierUnique(text)) { lineEdit->setStyleSheet("QLineEdit { background-color: rgba(255, 0, 0, 25%) }"); buttonBox.button(QDialogButtonBox::Ok)->setDisabled(true); } else { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 16d55c2d..bf9a91e8 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -2537,6 +2537,25 @@ void MainWindow::on_pushButton_SummaryChart_clicked() { openSubWindow(this->wildMonChart); } +void MainWindow::on_toolButton_WildMonSearch_clicked() { + if (!this->wildMonSearch) { + this->wildMonSearch = new WildMonSearch(this->editor->project, this); + connect(this->wildMonSearch, &WildMonSearch::openWildMonTableRequested, this, &MainWindow::openWildMonTable); + connect(this->editor, &Editor::wildMonTableEdited, this->wildMonSearch, &WildMonSearch::refresh); + } + openSubWindow(this->wildMonSearch); +} + +void MainWindow::openWildMonTable(const QString &mapName, const QString &groupName, const QString &fieldName) { + if (userSetMap(mapName)) { + // Switch to the correct main tab, wild encounter group, and wild encounter type tab. + on_mainTabBar_tabBarClicked(MainTab::WildPokemon); + ui->comboBox_EncounterGroupLabel->setCurrentText(groupName); + QWidget *w = ui->stackedWidget_WildMons->currentWidget(); + if (w) static_cast(w)->setCurrentField(fieldName); + } +} + void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() { editor->configureEncounterJSON(this); } @@ -2984,33 +3003,21 @@ void MainWindow::clearOverlay() { // delete is happening too late and some of the pointers haven't been cleared by the time we need them to, // so we nullify them all here anyway. bool MainWindow::closeSupplementaryWindows() { - if (this->tilesetEditor && !this->tilesetEditor->close()) - return false; - this->tilesetEditor = nullptr; + #define SAFE_CLOSE(window) \ + do { \ + if ((window) && !(window)->close()) \ + return false; \ + window = nullptr; \ + } while (0); - if (this->regionMapEditor && !this->regionMapEditor->close()) - return false; - this->regionMapEditor = nullptr; - - if (this->mapImageExporter && !this->mapImageExporter->close()) - return false; - this->mapImageExporter = nullptr; - - if (this->shortcutsEditor && !this->shortcutsEditor->close()) - return false; - this->shortcutsEditor = nullptr; - - if (this->preferenceEditor && !this->preferenceEditor->close()) - return false; - this->preferenceEditor = nullptr; - - if (this->customScriptsEditor && !this->customScriptsEditor->close()) - return false; - this->customScriptsEditor = nullptr; - - if (this->wildMonChart && !this->wildMonChart->close()) - return false; - this->wildMonChart = nullptr; + SAFE_CLOSE(this->tilesetEditor); + SAFE_CLOSE(this->regionMapEditor); + SAFE_CLOSE(this->mapImageExporter); + SAFE_CLOSE(this->shortcutsEditor); + SAFE_CLOSE(this->preferenceEditor); + SAFE_CLOSE(this->customScriptsEditor); + SAFE_CLOSE(this->wildMonChart); + SAFE_CLOSE(this->wildMonSearch); if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly(); this->projectSettingsEditor = nullptr; diff --git a/src/project.cpp b/src/project.cpp index 3abfcfbf..712ee87f 100644 --- a/src/project.cpp +++ b/src/project.cpp @@ -1721,11 +1721,11 @@ bool Project::readWildMonData() { for (OrderedJson subObjectRef : wildMonObj["wild_encounter_groups"].array_items()) { OrderedJson::object subObject = subObjectRef.object_items(); if (!subObject["for_maps"].bool_value()) { - extraEncounterGroups.push_back(subObject); + this->extraEncounterGroups.push_back(subObject); continue; } - for (OrderedJson field : subObject["fields"].array_items()) { + for (const OrderedJson &field : subObject["fields"].array_items()) { EncounterField encounterField; OrderedJson::object fieldObj = field.object_items(); encounterField.name = fieldObj["type"].string_value(); @@ -1744,17 +1744,17 @@ bool Project::readWildMonData() { } } encounterRateFrequencyMaps.insert(encounterField.name, QMap()); - wildMonFields.append(encounterField); + this->wildMonFields.append(encounterField); } auto encounters = subObject["encounters"].array_items(); - for (auto encounter : encounters) { + for (const auto &encounter : encounters) { OrderedJson::object encounterObj = encounter.object_items(); QString mapConstant = encounterObj["map"].string_value(); WildPokemonHeader header; - for (EncounterField monField : wildMonFields) { + for (const EncounterField &monField : this->wildMonFields) { QString field = monField.name; if (!encounterObj[field].is_null()) { OrderedJson::object encounterFieldObj = encounterObj[field].object_items(); @@ -1776,8 +1776,8 @@ bool Project::readWildMonData() { } } } - wildMonData[mapConstant].insert({encounterObj["base_label"].string_value(), header}); - encounterGroupLabels.append(encounterObj["base_label"].string_value()); + this->wildMonData[mapConstant].insert({encounterObj["base_label"].string_value(), header}); + this->encounterGroupLabels.append(encounterObj["base_label"].string_value()); } } @@ -1975,6 +1975,8 @@ bool Project::isIdentifierUnique(const QString &identifier) const { } if (identifier == getEmptyMapDefineName()) return false; + if (this->encounterGroupLabels.contains(identifier)) + return false; return true; } @@ -2981,6 +2983,31 @@ bool Project::readSpeciesIconPaths() { return true; } +QPixmap Project::getSpeciesIcon(const QString &species) const { + QPixmap pixmap; + if (!QPixmapCache::find(species, &pixmap)) { + // Prefer path from config. If not present, use the path parsed from project files + QString path = projectConfig.getPokemonIconPath(species); + if (path.isEmpty()) { + path = this->speciesToIconPath.value(species); + } else { + path = Project::getExistingFilepath(path); + } + + QImage img(path); + if (img.isNull()) { + // No icon for this species, use placeholder + static const QPixmap placeholder = QPixmap(QStringLiteral(":images/pokemon_icon_placeholder.png")); + pixmap = placeholder; + } else { + img.setColor(0, qRgba(0, 0, 0, 0)); + pixmap = QPixmap::fromImage(img).copy(0, 0, 32, 32); + QPixmapCache::insert(species, pixmap); + } + } + return pixmap; +} + int Project::getNumTilesPrimary() { return Project::num_tiles_primary; @@ -3067,19 +3094,21 @@ int Project::getMaxObjectEvents() } QString Project::getEmptyMapDefineName() { - const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix); - return prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_empty); + return projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix) + projectConfig.getIdentifier(ProjectIdentifier::define_map_empty); } QString Project::getDynamicMapDefineName() { - const QString prefix = projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix); - return prefix + projectConfig.getIdentifier(ProjectIdentifier::define_map_dynamic); + return projectConfig.getIdentifier(ProjectIdentifier::define_map_prefix) + projectConfig.getIdentifier(ProjectIdentifier::define_map_dynamic); } QString Project::getDynamicMapName() { return projectConfig.getIdentifier(ProjectIdentifier::symbol_dynamic_map_name); } +QString Project::getEmptySpeciesName() { + return projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix) + projectConfig.getIdentifier(ProjectIdentifier::define_species_empty); +} + // If the provided filepath is an absolute path to an existing file, return filepath. // If not, and the provided filepath is a relative path from the project dir to an existing file, return the relative path. // Otherwise return empty string. diff --git a/src/ui/encountertabledelegates.cpp b/src/ui/encountertabledelegates.cpp index 230abc7b..4ffe9e0d 100644 --- a/src/ui/encountertabledelegates.cpp +++ b/src/ui/encountertabledelegates.cpp @@ -12,30 +12,8 @@ SpeciesComboDelegate::SpeciesComboDelegate(Project *project, QObject *parent) : } void SpeciesComboDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const { - QString species = index.data(Qt::DisplayRole).toString(); - - QPixmap pm; - if (!QPixmapCache::find(species, &pm)) { - // Prefer path from config. If not present, use the path parsed from project files - QString path = projectConfig.getPokemonIconPath(species); - if (path.isEmpty()) { - path = this->project->speciesToIconPath.value(species); - } else { - path = Project::getExistingFilepath(path); - } - - QImage img(path); - if (img.isNull()) { - // No icon for this species, use placeholder - pm = QPixmap(":images/pokemon_icon_placeholder.png"); - } else { - img.setColor(0, qRgba(0, 0, 0, 0)); - pm = QPixmap::fromImage(img); - } - QPixmapCache::insert(species, pm); - } - QPixmap monIcon = pm.copy(0, 0, 32, 32); - + const QString species = index.data(Qt::DisplayRole).toString(); + const QPixmap monIcon = this->project->getSpeciesIcon(species); painter->drawText(QRect(option.rect.topLeft() + QPoint(36, 0), option.rect.bottomRight()), Qt::AlignLeft | Qt::AlignVCenter, species); painter->drawPixmap(QRect(option.rect.topLeft(), QSize(32, 32)), monIcon, monIcon.rect()); } diff --git a/src/ui/encountertablemodel.cpp b/src/ui/encountertablemodel.cpp index f7e73996..d3a6f2d3 100644 --- a/src/ui/encountertablemodel.cpp +++ b/src/ui/encountertablemodel.cpp @@ -3,52 +3,27 @@ -EncounterTableModel::EncounterTableModel(WildMonInfo info, EncounterFields fields, int index, QObject *parent) : QAbstractTableModel(parent) { - this->fieldIndex = index; - this->encounterFields = fields; - this->monInfo = info; - - this->resize(this->monInfo.wildPokemon.size(), ColumnType::Count); - - for (int r = 0; r < this->numRows; r++) { - this->groupNames.append(QString()); - this->slotPercentages.append(0.0); - this->slotRatios.append(fields[fieldIndex].encounterRates.value(r, 0)); - } - - if (!this->encounterFields[this->fieldIndex].groups.empty()) { - for (auto groupKeyPair : fields[fieldIndex].groups) { - int groupTotal = 0; - for (int i : groupKeyPair.second) { - this->groupNames[i] = groupKeyPair.first; - groupTotal += this->slotRatios[i]; - } - for (int i : groupKeyPair.second) { - this->slotPercentages[i] = static_cast(this->slotRatios[i]) / static_cast(groupTotal); - } - } - } else { - int groupTotal = 0; - for (int chance : this->encounterFields[this->fieldIndex].encounterRates) { - groupTotal += chance; - } - for (int i = 0; i < this->slotPercentages.count(); i++) { - this->slotPercentages[i] = static_cast(this->slotRatios[i]) / static_cast(groupTotal); +EncounterTableModel::EncounterTableModel(const WildMonInfo &info, const EncounterField &field, QObject *parent) + : QAbstractTableModel(parent), + m_numRows(info.wildPokemon.size()), + m_numCols(ColumnType::Count), + m_monInfo(info), + m_encounterField(field), + m_slotPercentages(getWildEncounterPercentages(field)) +{ + for (const auto &groupKeyPair : m_encounterField.groups) { + for (const auto &slot : groupKeyPair.second) { + m_groupNames.insert(slot, groupKeyPair.first); } } } -void EncounterTableModel::resize(int rows, int cols) { - this->numRows = rows; - this->numCols = cols; -} - int EncounterTableModel::rowCount(const QModelIndex &) const { - return this->numRows; + return m_numRows; } int EncounterTableModel::columnCount(const QModelIndex &) const { - return this->numCols; + return m_numCols; } QVariant EncounterTableModel::data(const QModelIndex &index, int role) const { @@ -61,26 +36,26 @@ QVariant EncounterTableModel::data(const QModelIndex &index, int role) const { return row; case ColumnType::Group: - return this->groupNames[row]; + return m_groupNames.value(row); case ColumnType::Species: - return this->monInfo.wildPokemon[row].species; + return m_monInfo.wildPokemon.value(row).species; case ColumnType::MinLevel: - return this->monInfo.wildPokemon[row].minLevel; + return m_monInfo.wildPokemon.value(row).minLevel; case ColumnType::MaxLevel: - return this->monInfo.wildPokemon[row].maxLevel; + return m_monInfo.wildPokemon.value(row).maxLevel; case ColumnType::EncounterChance: - return QString::number(this->slotPercentages[row] * 100.0, 'f', 2) + "%"; + return QString::number(m_slotPercentages.value(row, 0) * 100, 'f', 2) + "%"; case ColumnType::SlotRatio: - return this->slotRatios[row]; + return m_encounterField.encounterRates.value(row); case ColumnType::EncounterRate: if (row == 0) { - return this->monInfo.encounterRate; + return m_monInfo.encounterRate; } else { return QVariant(); } @@ -92,17 +67,17 @@ QVariant EncounterTableModel::data(const QModelIndex &index, int role) const { else if (role == Qt::EditRole) { switch (col) { case ColumnType::Species: - return this->monInfo.wildPokemon[row].species; + return m_monInfo.wildPokemon.value(row).species; case ColumnType::MinLevel: - return this->monInfo.wildPokemon[row].minLevel; + return m_monInfo.wildPokemon.value(row).minLevel; case ColumnType::MaxLevel: - return this->monInfo.wildPokemon[row].maxLevel; + return m_monInfo.wildPokemon.value(row).maxLevel; case ColumnType::EncounterRate: if (row == 0) { - return this->monInfo.encounterRate; + return m_monInfo.encounterRate; } else { return QVariant(); } @@ -149,12 +124,13 @@ bool EncounterTableModel::setData(const QModelIndex &index, const QVariant &valu int row = index.row(); int col = index.column(); + auto wildMon = &m_monInfo.wildPokemon[row]; switch (col) { case ColumnType::Species: { QString species = value.toString(); - if (this->monInfo.wildPokemon[row].species != species) { - this->monInfo.wildPokemon[row].species = species; + if (wildMon->species != species) { + wildMon->species = species; emit edited(); } break; @@ -162,9 +138,9 @@ bool EncounterTableModel::setData(const QModelIndex &index, const QVariant &valu case ColumnType::MinLevel: { int minLevel = value.toInt(); - if (this->monInfo.wildPokemon[row].minLevel != minLevel) { - this->monInfo.wildPokemon[row].minLevel = minLevel; - this->monInfo.wildPokemon[row].maxLevel = qMax(minLevel, this->monInfo.wildPokemon[row].maxLevel); + if (wildMon->minLevel != minLevel) { + wildMon->minLevel = minLevel; + wildMon->maxLevel = qMax(minLevel, wildMon->maxLevel); emit edited(); } break; @@ -172,9 +148,9 @@ bool EncounterTableModel::setData(const QModelIndex &index, const QVariant &valu case ColumnType::MaxLevel: { int maxLevel = value.toInt(); - if (this->monInfo.wildPokemon[row].maxLevel != maxLevel) { - this->monInfo.wildPokemon[row].maxLevel = maxLevel; - this->monInfo.wildPokemon[row].minLevel = qMin(maxLevel, this->monInfo.wildPokemon[row].minLevel); + if (wildMon->maxLevel != maxLevel) { + wildMon->maxLevel = maxLevel; + wildMon->minLevel = qMin(maxLevel, wildMon->minLevel); emit edited(); } break; @@ -182,8 +158,8 @@ bool EncounterTableModel::setData(const QModelIndex &index, const QVariant &valu case ColumnType::EncounterRate: { int encounterRate = value.toInt(); - if (this->monInfo.encounterRate != encounterRate) { - this->monInfo.encounterRate = encounterRate; + if (m_monInfo.encounterRate != encounterRate) { + m_monInfo.encounterRate = encounterRate; emit edited(); } break; diff --git a/src/ui/montabwidget.cpp b/src/ui/montabwidget.cpp index 7b196c75..3a04c1b7 100644 --- a/src/ui/montabwidget.cpp +++ b/src/ui/montabwidget.cpp @@ -29,6 +29,8 @@ void MonTabWidget::populate() { copyTabButtons.resize(fields.size()); copyTabButtons.fill(nullptr); + this->fieldNameToIndex.clear(); + int index = 0; for (EncounterField field : fields) { QTableView *table = new QTableView(this); @@ -45,7 +47,7 @@ void MonTabWidget::populate() { connect(buttonCopy, &QPushButton::clicked, [=]() {actionCopyTab(index); }); copyTabButtons[index] = buttonCopy; this->tabBar()->setTabButton(index, QTabBar::LeftSide, buttonCopy); - + this->fieldNameToIndex.insert(field.name, index); index++; } } @@ -111,7 +113,7 @@ void MonTabWidget::deactivateTab(int tabIndex) { EncounterTableModel *oldModel = static_cast(speciesTable->model()); WildMonInfo monInfo = oldModel->encounterData(); monInfo.active = false; - EncounterTableModel *newModel = new EncounterTableModel(monInfo, editor->project->wildMonFields, tabIndex, this); + EncounterTableModel *newModel = new EncounterTableModel(monInfo, editor->project->wildMonFields[tabIndex], this); speciesTable->setModel(newModel); setTabActive(tabIndex, false); @@ -120,7 +122,7 @@ void MonTabWidget::deactivateTab(int tabIndex) { void MonTabWidget::populateTab(int tabIndex, WildMonInfo monInfo) { QTableView *speciesTable = tableAt(tabIndex); - EncounterTableModel *model = new EncounterTableModel(monInfo, editor->project->wildMonFields, tabIndex, this); + EncounterTableModel *model = new EncounterTableModel(monInfo, editor->project->wildMonFields[tabIndex], this); connect(model, &EncounterTableModel::edited, editor, &Editor::saveEncounterTabData); connect(model, &EncounterTableModel::edited, editor, &Editor::wildMonTableEdited); speciesTable->setModel(model); @@ -167,3 +169,8 @@ void MonTabWidget::setTabActive(int index, bool active) { this->copyTabButtons[index]->show(); } } + +void MonTabWidget::setCurrentField(const QString &fieldName) { + int index = this->fieldNameToIndex.value(fieldName, -1); + if (index >= 0) setCurrentIndex(index); +} diff --git a/src/ui/wildmonsearch.cpp b/src/ui/wildmonsearch.cpp new file mode 100644 index 00000000..684d2947 --- /dev/null +++ b/src/ui/wildmonsearch.cpp @@ -0,0 +1,158 @@ +#include "wildmonsearch.h" +#include "ui_wildmonsearch.h" +#include "project.h" + +enum ResultsColumn { + Group, + Field, + Level, + Chance, +}; + +enum ResultsDataRole { + MapName = Qt::UserRole, +}; + +WildMonSearch::WildMonSearch(Project *project, QWidget *parent) : + QDialog(parent), + ui(new Ui::WildMonSearch), + project(project) +{ + setAttribute(Qt::WA_DeleteOnClose); + ui->setupUi(this); + + // Set up species combo box + ui->comboBox_Search->addItems(project->speciesToIconPath.keys()); + ui->comboBox_Search->setCurrentText(QString()); + ui->comboBox_Search->lineEdit()->setPlaceholderText(Project::getEmptySpeciesName()); + connect(ui->comboBox_Search, &QComboBox::currentTextChanged, this, &WildMonSearch::updateResults); + + // Set up table header + static const QStringList labels = {"Group", "Field", "Level", "Chance"}; + ui->table_Results->setHorizontalHeaderLabels(labels); + ui->table_Results->horizontalHeader()->setSectionResizeMode(ResultsColumn::Group, QHeaderView::Stretch); + ui->table_Results->horizontalHeader()->setSectionResizeMode(ResultsColumn::Field, QHeaderView::ResizeToContents); + ui->table_Results->horizontalHeader()->setSectionResizeMode(ResultsColumn::Level, QHeaderView::ResizeToContents); + ui->table_Results->horizontalHeader()->setSectionResizeMode(ResultsColumn::Chance, QHeaderView::ResizeToContents); + + // Table is read-only + ui->table_Results->setEditTriggers(QAbstractItemView::NoEditTriggers); + ui->table_Results->setSelectionMode(QAbstractItemView::NoSelection); + + connect(ui->table_Results, &QTableWidget::cellDoubleClicked, this, &WildMonSearch::cellDoubleClicked); + + refresh(); +} + +WildMonSearch::~WildMonSearch() { + delete ui; +} + +void WildMonSearch::refresh() { + this->resultsCache.clear(); + updatePercentageStrings(); + updateResults(ui->comboBox_Search->currentText()); +} + +void WildMonSearch::addTableEntry(const RowData &rowData) { + int row = ui->table_Results->rowCount(); + ui->table_Results->insertRow(row); + + auto groupItem = new QTableWidgetItem(rowData.groupName); + groupItem->setData(ResultsDataRole::MapName, rowData.mapName); + + ui->table_Results->setItem(row, ResultsColumn::Group, groupItem); + ui->table_Results->setItem(row, ResultsColumn::Field, new QTableWidgetItem(rowData.fieldName)); + ui->table_Results->setItem(row, ResultsColumn::Level, new QTableWidgetItem(rowData.levelRange)); + ui->table_Results->setItem(row, ResultsColumn::Chance, new QTableWidgetItem(rowData.chance)); +} + +QList WildMonSearch::search(const QString &species) const { + QList results; + for (const auto &keyPair : this->project->wildMonData) { + QString mapConstant = keyPair.first; + for (const auto &grouplLabelPair : this->project->wildMonData[mapConstant]) { + QString groupName = grouplLabelPair.first; + WildPokemonHeader encounterHeader = this->project->wildMonData[mapConstant][groupName]; + for (const auto &fieldNamePair : encounterHeader.wildMons) { + QString fieldName = fieldNamePair.first; + WildMonInfo monInfo = encounterHeader.wildMons[fieldName]; + for (int slot = 0; slot < monInfo.wildPokemon.length(); slot++) { + const WildPokemon wildMon = monInfo.wildPokemon.at(slot); + if (wildMon.species == species) { + RowData rowData; + rowData.groupName = groupName; + rowData.fieldName = fieldName; + rowData.mapName = this->project->mapConstantsToMapNames.value(mapConstant, mapConstant); + + // If min and max level are the same display a single number, otherwise display a level range. + rowData.levelRange = (wildMon.minLevel == wildMon.maxLevel) ? QString::number(wildMon.minLevel) + : QString("%1-%2").arg(wildMon.minLevel).arg(wildMon.maxLevel); + rowData.chance = this->percentageStrings[fieldName][slot]; + results.append(rowData); + } + } + } + } + } + return results; +} + +void WildMonSearch::updatePercentageStrings() { + this->percentageStrings.clear(); + for (const EncounterField &monField : this->project->wildMonFields) { + QMap slotToPercentString; + auto percentages = getWildEncounterPercentages(monField); + for (int i = 0; i < percentages.length(); i++) { + slotToPercentString.insert(i, QString::number(percentages.at(i) * 100, 'f', 2) + "%"); + } + this->percentageStrings[monField.name] = slotToPercentString; + } +} + +void WildMonSearch::updateResults(const QString &species) { + ui->speciesIcon->setPixmap(this->project->getSpeciesIcon(species)); + + ui->table_Results->clearContents(); + ui->table_Results->setRowCount(0); + + // Note: Per Qt docs, sorting should be disabled while populating the table to avoid it interfering with insertion order. + ui->table_Results->setSortingEnabled(false); + + if (ui->comboBox_Search->findText(species) < 0) + return; // Not a species name, no need to search wild encounter data. + + const QList results = this->resultsCache.value(species, search(species)); + if (results.isEmpty()) { + static const RowData noResults = { + .groupName = QStringLiteral("Species not found."), + .fieldName = QStringLiteral("--"), + .levelRange = QStringLiteral("--"), + .chance = QStringLiteral("--"), + }; + addTableEntry(noResults); + } else { + for (const auto &entry : results) { + addTableEntry(entry); + } + } + + // TODO: This does a lexical sort... We might need custom item delegates to get proper numerical sorting in this table. + ui->table_Results->setSortingEnabled(true); + + this->resultsCache.insert(species, results); +} + +// Double-clicking row data opens the corresponding map/table on the Wild Pokémon tab. +void WildMonSearch::cellDoubleClicked(int row, int) { + auto groupItem = ui->table_Results->item(row, ResultsColumn::Group); + auto fieldItem = ui->table_Results->item(row, ResultsColumn::Field); + if (!groupItem || !fieldItem) + return; + + const QString mapName = groupItem->data(ResultsDataRole::MapName).toString(); + if (mapName.isEmpty()) + return; + + emit openWildMonTableRequested(mapName, groupItem->text(), fieldItem->text()); +}