diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 9c997ad1..f71ab5c5 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -33,7 +33,7 @@ jobs:
uses: jurplel/install-qt-action@v2
with:
version: '5.14.2'
- modules: 'qtwidgets qtqml'
+ modules: 'qtwidgets qtqml qtcharts'
cached: ${{ steps.cache-qt.outputs.cache-hit }}
- name: Configure
@@ -58,7 +58,8 @@ jobs:
- name: Install Qt
uses: jurplel/install-qt-action@v3
with:
- version: '6.5.*'
+ version: '6.7.*'
+ modules: 'qtcharts'
cached: ${{ steps.cache-qt.outputs.cache-hit }}
- name: Configure
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d7d9f02..fef37e88 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
### Added
- Redesigned the Connections tab, adding a number of new features including the option to open or display diving maps and a list UI for easier edit access.
- Add a `Close Project` option
+- Add charts to the `Wild Pokémon` tab that show species and level distributions.
- An alert will be displayed when attempting to open a seemingly invalid project.
- Add support for defining project values with `enum` where `#define` was expected.
@@ -18,6 +19,7 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Changes to the "Mirror to Connecting Maps" setting will now be saved between sessions.
- A notice will be displayed when attempting to open the "Dynamic" map, rather than nothing happening.
- The base game version is now auto-detected if the project name contains only one of "emerald", "firered/leafgreen", or "ruby/sapphire".
+- The max encounter rate is now read from the project, rather than assuming the default value from RSE.
- It's now possible to cancel quitting if there are unsaved changes in sub-windows.
### Fixed
@@ -31,6 +33,8 @@ The **"Breaking Changes"** listed below are changes that have been made in the d
- Fix `About porymap` opening a new window each time it's activated.
- Fix the `Edit History` window not raising to the front when reactivated.
- New maps are now always inserted in map dropdowns at the correct position, rather than at the bottom of the list until the project is reloaded.
+- Fix invalid species names clearing from wild pokémon data when revisited.
+- Fix editing wild pokémon data not marking the map as edited.
- Fix changes to map connections not marking connected maps as unsaved.
- Fix numerous issues related to connecting a map to itself.
- Fix incorrect map connections getting selected when opening a map by double-clicking a map connection.
diff --git a/docsrc/manual/project-files.rst b/docsrc/manual/project-files.rst
index 20849ba3..b99e74d7 100644
--- a/docsrc/manual/project-files.rst
+++ b/docsrc/manual/project-files.rst
@@ -67,6 +67,7 @@ The filepath that Porymap expects for each file can be overridden on the ``Files
include/fieldmap.h, yes, no, ``constants_fieldmap``, reads tileset related constants
src/fieldmap.c, yes, no, ``fieldmap``, reads ``symbol_attribute_table``
src/event_object_movement.c, yes, no, ``initial_facing_table``, reads ``symbol_facing_directions``
+ src/wild_encounter.c, yes, no, ``wild_encounter``, reads ``define_max_encounter_rate``
src/pokemon_icon.c, yes, no, ``pokemon_icon_table``, reads files in ``symbol_pokemon_icon_table``
graphics/pokemon/\*/icon.png, yes, no, ``pokemon_gfx``, to search for Pokémon icons if they aren't found in ``symbol_pokemon_icon_table``
@@ -96,6 +97,7 @@ In addition to these files, there are some specific symbol and macro names that
``define_obj_event_count``, ``OBJECT_EVENT_TEMPLATES_COUNT``, to limit total Object Events
``define_min_level``, ``MIN_LEVEL``, minimum wild encounters level
``define_max_level``, ``MAX_LEVEL``, maximum wild encounters level
+ ``define_max_encounter_rate``, ``MAX_ENCOUNTER_RATE``, this value / 16 will be the maximum encounter rate on the ``Wild Pokémon`` tab
``define_tiles_primary``, ``NUM_TILES_IN_PRIMARY``,
``define_tiles_total``, ``NUM_TILES_TOTAL``,
``define_metatiles_primary``, ``NUM_METATILES_IN_PRIMARY``, total metatiles are calculated using metatile ID mask
@@ -120,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_map_section_count``, ``COUNT``, macro name after prefix for total number of region map sections
+ ``define_species_prefix``, ``SPECIES_``, expected prefix for species macro names
``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
@@ -134,4 +137,3 @@ In addition to these files, there are some specific symbol and macro names that
``regex_sign_facing_directions``, ``\bBG_EVENT_PLAYER_FACING_``, regex to find sign facing direction macro names
``regex_trainer_types``, ``\bTRAINER_TYPE_``, regex to find trainer type macro names
``regex_music``, ``\b(SE|MUS)_``, regex to find music macro names
- ``regex_species``, ``\bSPECIES_``, regex to find species macro names
diff --git a/forms/mainwindow.ui b/forms/mainwindow.ui
index 8c9059bb..f7253615 100644
--- a/forms/mainwindow.ui
+++ b/forms/mainwindow.ui
@@ -3004,6 +3004,13 @@
+ -
+
+
+ Summary Chart...
+
+
+
-
diff --git a/forms/wildmonchart.ui b/forms/wildmonchart.ui
new file mode 100644
index 00000000..488e066e
--- /dev/null
+++ b/forms/wildmonchart.ui
@@ -0,0 +1,245 @@
+
+
+ WildMonChart
+
+
+
+ 0
+ 0
+ 785
+ 492
+
+
+
+ Wild Pokémon Summary
+
+
+
-
+
+
+ QFrame::NoFrame
+
+
+ QFrame::Plain
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 4
+
+
-
+
+
+ Theme
+
+
+
+ -
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ ...
+
+
+
+ :/icons/help.ico:/icons/help.ico
+
+
+
+
+
+
+ -
+
+
+ 0
+
+
+
+ Species Distribution
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+ QPainter::Antialiasing
+
+
+
+
+
+
+
+ Level Distribution
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ QFrame::NoFrame
+
+
+ QFrame::Plain
+
+
+
+ 12
+
+
+ 0
+
+
-
+
+
+ Group
+
+
+
+ -
+
+
+ false
+
+
+ QComboBox::AdjustToMinimumContentsLength
+
+
+ 8
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+ -
+
+
+ Individual Mode
+
+
+ true
+
+
+ false
+
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
+ 4
+
+
-
+
+
+ Species
+
+
+
+ -
+
+
+ true
+
+
+ QComboBox::NoInsert
+
+
+ QComboBox::AdjustToMinimumContentsLengthWithIcon
+
+
+ 12
+
+
+
+
+
+
+
+
+
+ -
+
+
+ QPainter::Antialiasing
+
+
+
+
+
+
+
+
+
+
+
+ QChartView
+ QGraphicsView
+
+
+
+
+
+
+
+
diff --git a/include/config.h b/include/config.h
index 7486919f..a2fead2e 100644
--- a/include/config.h
+++ b/include/config.h
@@ -77,6 +77,7 @@ public:
this->monitorFiles = true;
this->tilesetCheckerboardFill = true;
this->theme = "default";
+ this->wildMonChartTheme = "";
this->textEditorOpenFolder = "";
this->textEditorGotoLine = "";
this->paletteEditorBitDepth = 24;
@@ -127,6 +128,7 @@ public:
bool monitorFiles;
bool tilesetCheckerboardFill;
QString theme;
+ QString wildMonChartTheme;
QString textEditorOpenFolder;
QString textEditorGotoLine;
int paletteEditorBitDepth;
@@ -136,6 +138,7 @@ public:
QDateTime lastUpdateCheckTime;
QVersionNumber lastUpdateCheckVersion;
QMap rateLimitTimes;
+ QByteArray wildMonChartGeometry;
protected:
virtual QString getConfigFilepath() override;
@@ -191,6 +194,7 @@ enum ProjectIdentifier {
define_obj_event_count,
define_min_level,
define_max_level,
+ define_max_encounter_rate,
define_tiles_primary,
define_tiles_total,
define_metatiles_primary,
@@ -215,6 +219,7 @@ enum ProjectIdentifier {
define_map_section_prefix,
define_map_section_empty,
define_map_section_count,
+ define_species_prefix,
regex_behaviors,
regex_obj_event_gfx,
regex_items,
@@ -229,7 +234,6 @@ enum ProjectIdentifier {
regex_sign_facing_directions,
regex_trainer_types,
regex_music,
- regex_species,
};
enum ProjectFilePath {
@@ -278,6 +282,7 @@ enum ProjectFilePath {
global_fieldmap,
fieldmap,
initial_facing_table,
+ wild_encounter,
pokemon_icon_table,
pokemon_gfx,
};
diff --git a/include/core/events.h b/include/core/events.h
index 8be1bbfc..2e00ef72 100644
--- a/include/core/events.h
+++ b/include/core/events.h
@@ -118,6 +118,8 @@ public:
}
}
+ static Event* create(Event::Type type);
+
static QMap icons;
// standard public methods
diff --git a/include/core/wildmoninfo.h b/include/core/wildmoninfo.h
index 7c16d1a0..6c93d0d2 100644
--- a/include/core/wildmoninfo.h
+++ b/include/core/wildmoninfo.h
@@ -22,9 +22,9 @@ struct WildPokemonHeader {
};
struct EncounterField {
- QString name;
+ QString name; // Ex: "fishing_mons"
QVector encounterRates;
- tsl::ordered_map> groups;
+ tsl::ordered_map> groups; // Ex: "good_rod", {2, 3, 4}
};
typedef QVector EncounterFields;
diff --git a/include/editor.h b/include/editor.h
index 89b6ccb5..7ef5ba10 100644
--- a/include/editor.h
+++ b/include/editor.h
@@ -26,6 +26,7 @@
#include "movablerect.h"
#include "cursortilerect.h"
#include "mapruler.h"
+#include "encountertablemodel.h"
class DraggablePixmapItem;
class MetatilesPixmapItem;
@@ -84,6 +85,7 @@ public:
void removeSelectedConnection();
void addNewWildMonGroup(QWidget *window);
void deleteWildMonGroup();
+ EncounterTableModel* getCurrentWildMonTable();
void updateDiveMap(QString mapName);
void updateEmergeMap(QString mapName);
void setSelectedConnection(MapConnection *connection);
@@ -95,8 +97,7 @@ public:
Tileset *getCurrentMapPrimaryTileset();
DraggablePixmapItem *addMapEvent(Event *event);
- void selectMapEvent(DraggablePixmapItem *object);
- void selectMapEvent(DraggablePixmapItem *object, bool toggle);
+ void selectMapEvent(DraggablePixmapItem *object, bool toggle = false);
DraggablePixmapItem *addNewEvent(Event::Type type);
void updateSelectedEvents();
void duplicateSelectedEvents();
@@ -223,7 +224,9 @@ private slots:
signals:
void objectsChanged();
void openConnectedMap(MapConnection*);
- void wildMonDataChanged();
+ void wildMonTableOpened(EncounterTableModel*);
+ void wildMonTableClosed();
+ void wildMonTableEdited();
void warpEventDoubleClicked(QString, int, Event::Group);
void currentMetatilesSelectionChanged();
void mapRulerStatusChanged(const QString &);
diff --git a/include/mainwindow.h b/include/mainwindow.h
index 01823945..c2726efc 100644
--- a/include/mainwindow.h
+++ b/include/mainwindow.h
@@ -27,6 +27,7 @@
#include "preferenceeditor.h"
#include "projectsettingseditor.h"
#include "customscriptseditor.h"
+#include "wildmonchart.h"
#include "updatepromoter.h"
#include "aboutporymap.h"
@@ -182,7 +183,6 @@ private slots:
void onOpenConnectedMap(MapConnection*);
void onMapNeedsRedrawing();
void onTilesetsSaved(QString, QString);
- void onWildMonDataChanged();
void openNewMapPopupWindow();
void onNewMapCreated();
void onMapCacheCleared();
@@ -290,6 +290,7 @@ private slots:
void on_horizontalSlider_CollisionZoom_valueChanged(int value);
void on_pushButton_NewWildMonGroup_clicked();
void on_pushButton_DeleteWildMonGroup_clicked();
+ void on_pushButton_SummaryChart_clicked();
void on_pushButton_ConfigureEncountersJSON_clicked();
void on_pushButton_CreatePrefab_clicked();
void on_spinBox_SelectedElevation_valueChanged(int elevation);
@@ -319,6 +320,7 @@ private:
QPointer updatePromoter = nullptr;
QPointer networkAccessManager = nullptr;
QPointer aboutWindow = nullptr;
+ QPointer wildMonChart = nullptr;
FilterChildrenProxyModel *mapListProxyModel;
QStandardItemModel *mapListModel;
QList *mapGroupItemsList;
diff --git a/include/project.h b/include/project.h
index 8c08e508..c3fc1677 100644
--- a/include/project.h
+++ b/include/project.h
@@ -39,7 +39,6 @@ public:
QMap mapGroups;
QList groupedMapNames;
QStringList mapNames;
- QMap miscConstants;
QList healLocations;
QMap healLocationNameToValue;
QMap mapConstantsToMapNames;
@@ -79,6 +78,9 @@ public:
bool usingAsmTilesets;
QString importExportPath;
QSet disabledSettingsNames;
+ int pokemonMinLevel;
+ int pokemonMaxLevel;
+ int maxEncounterRate;
bool wildEncountersLoaded;
void set_root(QString);
diff --git a/include/ui/customscriptseditor.h b/include/ui/customscriptseditor.h
index 27238c4e..0e5d3489 100644
--- a/include/ui/customscriptseditor.h
+++ b/include/ui/customscriptseditor.h
@@ -55,7 +55,8 @@ private slots:
void dialogButtonClicked(QAbstractButton *button);
void createNewScript();
void loadScript();
- void refreshScripts();
+ bool refreshScripts();
+ void userRefreshScripts();
void removeSelectedScripts();
void openSelectedScripts();
};
diff --git a/include/ui/draggablepixmapitem.h b/include/ui/draggablepixmapitem.h
index 9c318962..aeda4daf 100644
--- a/include/ui/draggablepixmapitem.h
+++ b/include/ui/draggablepixmapitem.h
@@ -24,13 +24,7 @@ public:
updatePosition();
}
- Editor *editor = nullptr;
Event *event = nullptr;
- QGraphicsItemAnimation *pos_anim = nullptr;
-
- bool active;
- int last_x;
- int last_y;
void updatePosition();
void move(int dx, int dy);
@@ -38,6 +32,12 @@ public:
void emitPositionChanged();
void updatePixmap();
+private:
+ Editor *editor = nullptr;
+ QPoint lastPos;
+ bool active = false;
+ bool releaseSelectionQueued = false;
+
signals:
void positionChanged(Event *event);
void xChanged(int);
diff --git a/include/ui/encountertablemodel.h b/include/ui/encountertablemodel.h
index 6aacbf30..b2a39ad8 100644
--- a/include/ui/encountertablemodel.h
+++ b/include/ui/encountertablemodel.h
@@ -28,7 +28,9 @@ public:
Slot, Group, Species, MinLevel, MaxLevel, EncounterChance, SlotRatio, EncounterRate, Count
};
- WildMonInfo encounterData();
+ 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);
private:
diff --git a/include/ui/wildmonchart.h b/include/ui/wildmonchart.h
new file mode 100644
index 00000000..d4e3d83f
--- /dev/null
+++ b/include/ui/wildmonchart.h
@@ -0,0 +1,98 @@
+#ifndef WILDMONCHART_H
+#define WILDMONCHART_H
+
+#include "encountertablemodel.h"
+
+#include
+
+#if __has_include()
+#include
+
+namespace Ui {
+class WildMonChart;
+}
+
+class WildMonChart : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit WildMonChart(QWidget *parent, const EncounterTableModel *table);
+ ~WildMonChart();
+
+ virtual void closeEvent(QCloseEvent *event) override;
+
+public slots:
+ void setTable(const EncounterTableModel *table);
+ void clearTable();
+ void refresh();
+
+private:
+ Ui::WildMonChart *ui;
+ const EncounterTableModel *table;
+
+ QStringList groupNames;
+ QStringList groupNamesReversed;
+ QStringList speciesInLegendOrder;
+ QMap tableIndexToGroupName;
+
+ struct LevelRange {
+ int min;
+ int max;
+ };
+ QMap groupedLevelRanges;
+
+ struct Summary {
+ double speciesFrequency = 0.0;
+ QMap levelFrequencies;
+ };
+ typedef QMap GroupedData;
+
+ QMap speciesToGroupedData;
+ QMap speciesToColor;
+
+
+ QStringList getSpeciesNamesAlphabetical() const;
+ double getSpeciesFrequency(const QString&, const QString&) const;
+ QMap getLevelFrequencies(const QString &, const QString &) const;
+ LevelRange getLevelRange(const QString &, const QString &) const;
+ bool usesGroupLabels() const;
+
+ void clearTableData();
+ void readTable();
+ QChart* createSpeciesDistributionChart();
+ QChart* createLevelDistributionChart();
+ QBarSet* createLevelDistributionBarSet(const QString &, const QString &, bool);
+ void refreshSpeciesDistributionChart();
+ void refreshLevelDistributionChart();
+
+ void saveSpeciesColors(const QList &);
+ void applySpeciesColors(const QList &);
+ QChart::ChartTheme currentTheme() const;
+ void updateTheme();
+ void limitChartAnimation(QChart*);
+
+ void showHelpDialog();
+};
+
+#else
+
+// As of writing our static Qt build for Windows doesn't include the QtCharts module, so we dummy the class out here.
+// The charts module is additionally excluded from Windows in porymap.pro
+#define DISABLE_CHARTS_MODULE
+
+class WildMonChart : public QWidget
+{
+ Q_OBJECT
+public:
+ explicit WildMonChart(QWidget *, const EncounterTableModel *) {};
+ ~WildMonChart() {};
+
+public slots:
+ void setTable(const EncounterTableModel *) {};
+ void clearTable() {};
+ void refresh() {};
+};
+
+#endif // __has_include()
+
+#endif // WILDMONCHART_H
diff --git a/porymap.pro b/porymap.pro
index 744edaef..f36f536f 100644
--- a/porymap.pro
+++ b/porymap.pro
@@ -6,6 +6,10 @@
QT += core gui qml network
+!win32 {
+ QT += charts
+}
+
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = porymap
@@ -110,7 +114,8 @@ SOURCES += src/core/block.cpp \
src/settings.cpp \
src/log.cpp \
src/ui/uintspinbox.cpp \
- src/ui/updatepromoter.cpp
+ src/ui/updatepromoter.cpp \
+ src/ui/wildmonchart.cpp
HEADERS += include/core/block.h \
include/core/bitpacker.h \
@@ -209,7 +214,8 @@ HEADERS += include/core/block.h \
include/settings.h \
include/log.h \
include/ui/uintspinbox.h \
- include/ui/updatepromoter.h
+ include/ui/updatepromoter.h \
+ include/ui/wildmonchart.h
FORMS += forms/mainwindow.ui \
forms/connectionslistitem.ui \
@@ -230,7 +236,8 @@ FORMS += forms/mainwindow.ui \
forms/projectsettingseditor.ui \
forms/customscriptseditor.ui \
forms/customscriptslistitem.ui \
- forms/updatepromoter.ui
+ forms/updatepromoter.ui \
+ forms/wildmonchart.ui
RESOURCES += \
resources/images.qrc \
diff --git a/resources/icons/help.ico b/resources/icons/help.ico
new file mode 100755
index 00000000..68f51bac
Binary files /dev/null and b/resources/icons/help.ico differ
diff --git a/resources/images.qrc b/resources/images.qrc
index bdac1a47..73ed0620 100644
--- a/resources/images.qrc
+++ b/resources/images.qrc
@@ -16,6 +16,7 @@
icons/folder_map_opened.ico
icons/folder_map.ico
icons/folder.ico
+ icons/help.ico
icons/map_edited.ico
icons/map_opened.ico
icons/map.ico
diff --git a/src/config.cpp b/src/config.cpp
index 818dd714..20869171 100644
--- a/src/config.cpp
+++ b/src/config.cpp
@@ -85,6 +85,7 @@ const QMap> ProjectConfig::defaultIde
{ProjectIdentifier::define_obj_event_count, {"define_obj_event_count", "OBJECT_EVENT_TEMPLATES_COUNT"}},
{ProjectIdentifier::define_min_level, {"define_min_level", "MIN_LEVEL"}},
{ProjectIdentifier::define_max_level, {"define_max_level", "MAX_LEVEL"}},
+ {ProjectIdentifier::define_max_encounter_rate, {"define_max_encounter_rate", "MAX_ENCOUNTER_RATE"}},
{ProjectIdentifier::define_tiles_primary, {"define_tiles_primary", "NUM_TILES_IN_PRIMARY"}},
{ProjectIdentifier::define_tiles_total, {"define_tiles_total", "NUM_TILES_TOTAL"}},
{ProjectIdentifier::define_metatiles_primary, {"define_metatiles_primary", "NUM_METATILES_IN_PRIMARY"}},
@@ -109,6 +110,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_map_section_count, {"define_map_section_count", "COUNT"}},
+ {ProjectIdentifier::define_species_prefix, {"define_species_prefix", "SPECIES_"}},
// Regex
{ProjectIdentifier::regex_behaviors, {"regex_behaviors", "\\bMB_"}},
{ProjectIdentifier::regex_obj_event_gfx, {"regex_obj_event_gfx", "\\bOBJ_EVENT_GFX_"}},
@@ -124,7 +126,6 @@ const QMap> ProjectConfig::defaultIde
{ProjectIdentifier::regex_sign_facing_directions, {"regex_sign_facing_directions", "\\bBG_EVENT_PLAYER_FACING_"}},
{ProjectIdentifier::regex_trainer_types, {"regex_trainer_types", "\\bTRAINER_TYPE_"}},
{ProjectIdentifier::regex_music, {"regex_music", "\\b(SE|MUS)_"}},
- {ProjectIdentifier::regex_species, {"regex_species", "\\bSPECIES_"}},
};
const QMap> ProjectConfig::defaultPaths = {
@@ -174,6 +175,7 @@ const QMap> ProjectConfig::defaultPaths
{ProjectFilePath::fieldmap, { "fieldmap", "src/fieldmap.c"}},
{ProjectFilePath::pokemon_icon_table, { "pokemon_icon_table", "src/pokemon_icon.c"}},
{ProjectFilePath::initial_facing_table, { "initial_facing_table", "src/event_object_movement.c"}},
+ {ProjectFilePath::wild_encounter, { "wild_encounter", "src/wild_encounter.c"}},
{ProjectFilePath::pokemon_gfx, { "pokemon_gfx", "graphics/pokemon/"}},
};
@@ -364,6 +366,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
this->customScriptsEditorGeometry = bytesFromString(value);
} else if (key == "custom_scripts_editor_state") {
this->customScriptsEditorState = bytesFromString(value);
+ } else if (key == "wild_mon_chart_geometry") {
+ this->wildMonChartGeometry = bytesFromString(value);
} else if (key == "metatiles_zoom") {
this->metatilesZoom = getConfigInteger(key, value, 10, 100, 30);
} else if (key == "collision_zoom") {
@@ -390,6 +394,8 @@ void PorymapConfig::parseConfigKeyValue(QString key, QString value) {
this->tilesetCheckerboardFill = getConfigBool(key, value);
} else if (key == "theme") {
this->theme = value;
+ } else if (key == "wild_mon_chart_theme") {
+ this->wildMonChartTheme = value;
} else if (key == "text_editor_open_directory") {
this->textEditorOpenFolder = value;
} else if (key == "text_editor_goto_line") {
@@ -449,6 +455,7 @@ QMap PorymapConfig::getKeyValueMap() {
map.insert("project_settings_editor_state", stringFromByteArray(this->projectSettingsEditorState));
map.insert("custom_scripts_editor_geometry", stringFromByteArray(this->customScriptsEditorGeometry));
map.insert("custom_scripts_editor_state", stringFromByteArray(this->customScriptsEditorState));
+ map.insert("wild_mon_chart_geometry", stringFromByteArray(this->wildMonChartGeometry));
map.insert("mirror_connecting_maps", this->mirrorConnectingMaps ? "1" : "0");
map.insert("show_dive_emerge_maps", this->showDiveEmergeMaps ? "1" : "0");
map.insert("dive_emerge_map_opacity", QString::number(this->diveEmergeMapOpacity));
@@ -468,6 +475,7 @@ QMap PorymapConfig::getKeyValueMap() {
map.insert("monitor_files", this->monitorFiles ? "1" : "0");
map.insert("tileset_checkerboard_fill", this->tilesetCheckerboardFill ? "1" : "0");
map.insert("theme", this->theme);
+ map.insert("wild_mon_chart_theme", this->wildMonChartTheme);
map.insert("text_editor_open_directory", this->textEditorOpenFolder);
map.insert("text_editor_goto_line", this->textEditorGotoLine);
map.insert("palette_editor_bit_depth", QString::number(this->paletteEditorBitDepth));
diff --git a/src/core/editcommands.cpp b/src/core/editcommands.cpp
index 640cb43f..f557dddd 100644
--- a/src/core/editcommands.cpp
+++ b/src/core/editcommands.cpp
@@ -331,7 +331,7 @@ void EventCreate::redo() {
// select this event
editor->selected_events->clear();
- editor->selectMapEvent(event->getPixmapItem(), false);
+ editor->selectMapEvent(event->getPixmapItem());
}
void EventCreate::undo() {
diff --git a/src/core/events.cpp b/src/core/events.cpp
index b0ea3e39..13be4153 100644
--- a/src/core/events.cpp
+++ b/src/core/events.cpp
@@ -6,6 +6,21 @@
QMap Event::icons;
+Event* Event::create(Event::Type type) {
+ switch (type) {
+ case Event::Type::Object: return new ObjectEvent();
+ case Event::Type::CloneObject: return new CloneObjectEvent();
+ case Event::Type::Warp: return new WarpEvent();
+ case Event::Type::Trigger: return new TriggerEvent();
+ case Event::Type::WeatherTrigger: return new WeatherTriggerEvent();
+ case Event::Type::Sign: return new SignEvent();
+ case Event::Type::HiddenItem: return new HiddenItemEvent();
+ case Event::Type::SecretBase: return new SecretBaseEvent();
+ case Event::Type::HealLocation: return new HealLocationEvent();
+ default: return nullptr;
+ }
+}
+
Event::~Event() {
if (this->eventFrame)
this->eventFrame->deleteLater();
diff --git a/src/editor.cpp b/src/editor.cpp
index 22957790..69b2ffb9 100644
--- a/src/editor.cpp
+++ b/src/editor.cpp
@@ -7,7 +7,6 @@
#include "mapsceneeventfilter.h"
#include "metatile.h"
#include "montabwidget.h"
-#include "encountertablemodel.h"
#include "editcommands.h"
#include "config.h"
#include "scripting.h"
@@ -43,6 +42,11 @@ Editor::Editor(Ui::MainWindow* ui)
selectNewEvents = false;
}
});
+
+ // Send signals used for updating the wild pokemon summary chart
+ connect(ui->stackedWidget_WildMons, &QStackedWidget::currentChanged, [this] {
+ emit wildMonTableOpened(getCurrentWildMonTable());
+ });
}
Editor::~Editor()
@@ -79,9 +83,7 @@ void Editor::saveUiFields() {
}
void Editor::setProject(Project * project) {
- if (this->project) {
- closeProject();
- }
+ closeProject();
this->project = project;
MapConnection::project = project;
}
@@ -194,6 +196,7 @@ void Editor::setEditingConnections() {
void Editor::clearWildMonTables() {
QStackedWidget *stack = ui->stackedWidget_WildMons;
+ const QSignalBlocker blocker(stack);
// delete widgets from previous map data if they exist
while (stack->count()) {
@@ -203,6 +206,7 @@ void Editor::clearWildMonTables() {
}
ui->comboBox_EncounterGroupLabel->clear();
+ emit wildMonTableClosed();
}
void Editor::displayWildMonTables() {
@@ -243,8 +247,12 @@ void Editor::displayWildMonTables() {
}
tabIndex++;
}
+ connect(tabWidget, &MonTabWidget::currentChanged, [this] {
+ emit wildMonTableOpened(getCurrentWildMonTable());
+ });
}
stack->setCurrentIndex(0);
+ emit wildMonTableOpened(getCurrentWildMonTable());
}
void Editor::addNewWildMonGroup(QWidget *window) {
@@ -359,7 +367,7 @@ void Editor::addNewWildMonGroup(QWidget *window) {
tabIndex++;
}
saveEncounterTabData();
- emit wildMonDataChanged();
+ emit wildMonTableEdited();
}
}
@@ -397,7 +405,8 @@ void Editor::deleteWildMonGroup() {
project->encounterGroupLabels.remove(i);
displayWildMonTables();
- emit wildMonDataChanged();
+ saveEncounterTabData();
+ emit wildMonTableEdited();
}
}
@@ -650,7 +659,8 @@ void Editor::configureEncounterJSON(QWidget *window) {
// Re-draw the tab accordingly.
displayWildMonTables();
- emit wildMonDataChanged();
+ saveEncounterTabData();
+ emit wildMonTableEdited();
}
}
@@ -684,6 +694,16 @@ void Editor::saveEncounterTabData() {
}
}
+EncounterTableModel* Editor::getCurrentWildMonTable() {
+ auto tabWidget = static_cast(ui->stackedWidget_WildMons->currentWidget());
+ if (!tabWidget) return nullptr;
+
+ auto tableView = tabWidget->tableAt(tabWidget->currentIndex());
+ if (!tableView) return nullptr;
+
+ return static_cast(tableView->model());
+}
+
void Editor::updateEncounterFields(EncounterFields newFields) {
EncounterFields oldFields = project->wildMonFields;
// Go through fields and determine whether we need to update a field.
@@ -1358,7 +1378,7 @@ void Editor::mouseEvent_map(QGraphicsSceneMouseEvent *event, MapPixmapItem *item
if (newEvent) {
newEvent->move(pos.x(), pos.y());
emit objectsChanged();
- selectMapEvent(newEvent, false);
+ selectMapEvent(newEvent);
}
}
}
@@ -1994,10 +2014,6 @@ void Editor::updateSelectedEvents() {
emit objectsChanged();
}
-void Editor::selectMapEvent(DraggablePixmapItem *object) {
- selectMapEvent(object, false);
-}
-
void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) {
if (!selected_events || !object)
return;
@@ -2007,10 +2023,10 @@ void Editor::selectMapEvent(DraggablePixmapItem *object, bool toggle) {
selected_events->clear();
selected_events->append(object);
} else if (!selected_events->contains(object)) {
- // Adding event to selection
+ // Adding event to group selection
selected_events->append(object);
} else if (selected_events->length() > 1) {
- // Removing from group selection
+ // Removing event from group selection
selected_events->removeOne(object);
} else {
// Attempting to toggle the only currently-selected event.
@@ -2072,56 +2088,24 @@ void Editor::duplicateSelectedEvents() {
}
DraggablePixmapItem *Editor::addNewEvent(Event::Type type) {
- Event *event = nullptr;
+ if (!project || !map || eventLimitReached(type))
+ return nullptr;
- if (project && map && !eventLimitReached(type)) {
- switch (type) {
- case Event::Type::Object:
- event = new ObjectEvent();
- break;
- case Event::Type::CloneObject:
- event = new CloneObjectEvent();
- break;
- case Event::Type::Warp:
- event = new WarpEvent();
- break;
- case Event::Type::Trigger:
- event = new TriggerEvent();
- break;
- case Event::Type::WeatherTrigger:
- event = new WeatherTriggerEvent();
- break;
- case Event::Type::Sign:
- event = new SignEvent();
- break;
- case Event::Type::HiddenItem:
- event = new HiddenItemEvent();
- break;
- case Event::Type::SecretBase:
- event = new SecretBaseEvent();
- break;
- case Event::Type::HealLocation: {
- event = new HealLocationEvent();
- event->setMap(this->map);
- event->setDefaultValues(this->project);
- HealLocation healLocation = HealLocation::fromEvent(event);
- project->healLocations.append(healLocation);
- ((HealLocationEvent *)event)->setIndex(project->healLocations.length());
- break;
- }
- default:
- break;
- }
- if (!event) return nullptr;
+ Event *event = Event::create(type);
+ if (!event)
+ return nullptr;
- event->setMap(this->map);
- event->setDefaultValues(this->project);
+ event->setMap(this->map);
+ event->setDefaultValues(this->project);
- map->editHistory.push(new EventCreate(this, map, event));
- return event->getPixmapItem();
+ if (type == Event::Type::HealLocation) {
+ HealLocation healLocation = HealLocation::fromEvent(event);
+ project->healLocations.append(healLocation);
+ ((HealLocationEvent *)event)->setIndex(project->healLocations.length());
}
- return nullptr;
+ map->editHistory.push(new EventCreate(this, map, event));
+ return event->getPixmapItem();
}
// Currently only object events have an explicit limit
@@ -2228,10 +2212,8 @@ void Editor::objectsView_onMousePress(QMouseEvent *event) {
bool multiSelect = event->modifiers() & Qt::ControlModifier;
if (!selectingEvent && !multiSelect && selected_events->length() > 1) {
- DraggablePixmapItem *first = selected_events->first();
- selected_events->clear();
- selected_events->append(first);
- updateSelectedEvents();
+ // User is clearing group selection by clicking on the background
+ this->selectMapEvent(selected_events->first());
}
selectingEvent = false;
}
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 23b7eb06..ecac1703 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -124,6 +124,10 @@ void MainWindow::initWindow() {
ui->actionCheck_for_Updates->setVisible(false);
#endif
+#ifdef DISABLE_CHARTS_MODULE
+ ui->pushButton_SummaryChart->setVisible(false);
+#endif
+
setWindowDisabled(true);
}
@@ -239,10 +243,8 @@ void MainWindow::initExtraSignals() {
connect(ui->tabWidget_EventType, &QTabWidget::currentChanged, this, &MainWindow::eventTabChanged);
// Change pages on wild encounter groups
- QStackedWidget *stack = ui->stackedWidget_WildMons;
- QComboBox *labelCombo = ui->comboBox_EncounterGroupLabel;
- connect(labelCombo, QOverload::of(&QComboBox::currentIndexChanged), [=](int index){
- stack->setCurrentIndex(index);
+ connect(ui->comboBox_EncounterGroupLabel, QOverload::of(&QComboBox::currentIndexChanged), [this](int index){
+ ui->stackedWidget_WildMons->setCurrentIndex(index);
});
// Convert the layout of the map tools' frame into an adjustable FlowLayout
@@ -309,7 +311,7 @@ void MainWindow::initEditor() {
connect(this->editor, &Editor::openConnectedMap, this, &MainWindow::onOpenConnectedMap);
connect(this->editor, &Editor::warpEventDoubleClicked, this, &MainWindow::openWarpMap);
connect(this->editor, &Editor::currentMetatilesSelectionChanged, this, &MainWindow::currentMetatilesSelectionChanged);
- connect(this->editor, &Editor::wildMonDataChanged, this, &MainWindow::onWildMonDataChanged);
+ connect(this->editor, &Editor::wildMonTableEdited, [this] { this->markMapEdited(); });
connect(this->editor, &Editor::mapRulerStatusChanged, this, &MainWindow::onMapRulerStatusChanged);
connect(this->editor, &Editor::tilesetUpdated, this, &Scripting::cb_TilesetUpdated);
connect(ui->toolButton_Open_Scripts, &QToolButton::pressed, this->editor, &Editor::openMapScripts);
@@ -1113,6 +1115,7 @@ void MainWindow::clearProjectUI() {
// Clear map list
mapListModel->clear();
+ mapListIndexes.clear();
mapGroupItemsList->clear();
}
@@ -1129,6 +1132,7 @@ void MainWindow::sortMapList() {
ui->mapList->setUpdatesEnabled(false);
mapListModel->clear();
+ mapListIndexes.clear();
mapGroupItemsList->clear();
QStandardItem *root = mapListModel->invisibleRootItem();
@@ -1761,7 +1765,7 @@ void MainWindow::paste() {
editor->metatile_selector_item->setExternalSelection(width, height, metatiles, collisions);
break;
}
- case 1:
+ case MainTab::Events:
{
// can only paste events to this tab
if (pasteObject["object"].toString() != "events") {
@@ -1773,56 +1777,25 @@ void MainWindow::paste() {
QJsonArray events = pasteObject["events"].toArray();
for (QJsonValue event : events) {
// paste the event to the map
- Event *pasteEvent = nullptr;
-
- Event::Type type = Event::eventTypeFromString(event["event_type"].toString());
+ const QString typeString = event["event_type"].toString();
+ Event::Type type = Event::eventTypeFromString(typeString);
if (this->editor->eventLimitReached(type)) {
- logWarn(QString("Cannot paste event, the limit for type '%1' has been reached.").arg(event["event_type"].toString()));
- break;
+ logWarn(QString("Cannot paste event, the limit for type '%1' has been reached.").arg(typeString));
+ continue;
+ }
+ if (type == Event::Type::HealLocation) {
+ logWarn(QString("Cannot paste events of type '%1'").arg(typeString));
+ continue;
}
- switch (type) {
- case Event::Type::Object:
- pasteEvent = new ObjectEvent();
- pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
- break;
- case Event::Type::CloneObject:
- pasteEvent = new CloneObjectEvent();
- pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
- break;
- case Event::Type::Warp:
- pasteEvent = new WarpEvent();
- pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
- break;
- case Event::Type::Trigger:
- pasteEvent = new TriggerEvent();
- pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
- break;
- case Event::Type::WeatherTrigger:
- pasteEvent = new WeatherTriggerEvent();
- pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
- break;
- case Event::Type::Sign:
- pasteEvent = new SignEvent();
- pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
- break;
- case Event::Type::HiddenItem:
- pasteEvent = new HiddenItemEvent();
- pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
- break;
- case Event::Type::SecretBase:
- pasteEvent = new SecretBaseEvent();
- pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
- break;
- default:
- break;
- }
+ Event *pasteEvent = Event::create(type);
+ if (!pasteEvent)
+ continue;
- if (pasteEvent) {
- pasteEvent->setMap(this->editor->map);
- newEvents.append(pasteEvent);
- }
+ pasteEvent->loadFromJson(event["event"].toObject(), this->editor->project);
+ pasteEvent->setMap(this->editor->map);
+ newEvents.append(pasteEvent);
}
if (!newEvents.empty()) {
@@ -2014,7 +1987,7 @@ void MainWindow::addNewEvent(Event::Type type) {
auto centerPos = ui->graphicsView_Map->mapToScene(halfSize.width(), halfSize.height());
object->moveTo(Metatile::coordFromPixmapCoord(centerPos));
updateObjects();
- editor->selectMapEvent(object, false);
+ editor->selectMapEvent(object);
} else {
QMessageBox msgBox(this);
msgBox.setText("Failed to add new event");
@@ -2562,11 +2535,6 @@ void MainWindow::onTilesetsSaved(QString primaryTilesetLabel, QString secondaryT
redrawMapScene();
}
-void MainWindow::onWildMonDataChanged() {
- editor->saveEncounterTabData();
- markMapEdited();
-}
-
void MainWindow::onMapRulerStatusChanged(const QString &status) {
if (status.isEmpty()) {
label_MapRulerStatus->hide();
@@ -2659,6 +2627,16 @@ void MainWindow::on_pushButton_DeleteWildMonGroup_clicked() {
editor->deleteWildMonGroup();
}
+void MainWindow::on_pushButton_SummaryChart_clicked() {
+ if (!this->wildMonChart) {
+ this->wildMonChart = new WildMonChart(this, this->editor->getCurrentWildMonTable());
+ connect(this->editor, &Editor::wildMonTableOpened, this->wildMonChart, &WildMonChart::setTable);
+ connect(this->editor, &Editor::wildMonTableClosed, this->wildMonChart, &WildMonChart::clearTable);
+ connect(this->editor, &Editor::wildMonTableEdited, this->wildMonChart, &WildMonChart::refresh);
+ }
+ openSubWindow(this->wildMonChart);
+}
+
void MainWindow::on_pushButton_ConfigureEncountersJSON_clicked() {
editor->configureEncounterJSON(this);
}
@@ -3115,6 +3093,10 @@ bool MainWindow::closeSupplementaryWindows() {
return false;
this->customScriptsEditor = nullptr;
+ if (this->wildMonChart && !this->wildMonChart->close())
+ return false;
+ this->wildMonChart = nullptr;
+
if (this->projectSettingsEditor) this->projectSettingsEditor->closeQuietly();
this->projectSettingsEditor = nullptr;
diff --git a/src/project.cpp b/src/project.cpp
index 8c1dbba5..bfeaab38 100644
--- a/src/project.cpp
+++ b/src/project.cpp
@@ -1654,15 +1654,43 @@ void Project::deleteFile(QString path) {
}
bool Project::readWildMonData() {
- extraEncounterGroups.clear();
- wildMonFields.clear();
- wildMonData.clear();
- encounterGroupLabels.clear();
+ this->extraEncounterGroups.clear();
+ this->wildMonFields.clear();
+ this->wildMonData.clear();
+ this->encounterGroupLabels.clear();
+ this->pokemonMinLevel = 0;
+ this->pokemonMaxLevel = 100;
+ this->maxEncounterRate = 2880/16;
this->wildEncountersLoaded = false;
if (!userConfig.useEncounterJson) {
return true;
}
+ // Read max encounter rate. The games multiply the encounter rate value in the map data by 16, so our input limit is the max/16.
+ const QString encounterRateFile = projectConfig.getFilePath(ProjectFilePath::wild_encounter);
+ const QString maxEncounterRateName = projectConfig.getIdentifier(ProjectIdentifier::define_max_encounter_rate);
+
+ fileWatcher.addPath(QString("%1/%2").arg(root).arg(encounterRateFile));
+ auto defines = parser.readCDefinesByName(encounterRateFile, {maxEncounterRateName});
+ if (defines.contains(maxEncounterRateName))
+ this->maxEncounterRate = defines.value(maxEncounterRateName)/16;
+
+ // Read min/max level
+ const QString levelRangeFile = projectConfig.getFilePath(ProjectFilePath::constants_pokemon);
+ const QString minLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_min_level);
+ const QString maxLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_max_level);
+
+ fileWatcher.addPath(QString("%1/%2").arg(root).arg(levelRangeFile));
+ defines = parser.readCDefinesByName(levelRangeFile, {minLevelName, maxLevelName});
+ if (defines.contains(minLevelName))
+ this->pokemonMinLevel = defines.value(minLevelName);
+ if (defines.contains(maxLevelName))
+ this->pokemonMaxLevel = defines.value(maxLevelName);
+
+ this->pokemonMinLevel = qMin(this->pokemonMinLevel, this->pokemonMaxLevel);
+ this->pokemonMaxLevel = qMax(this->pokemonMinLevel, this->pokemonMaxLevel);
+
+ // Read encounter data
QString wildMonJsonFilepath = QString("%1/%2").arg(root).arg(projectConfig.getFilePath(ProjectFilePath::json_wild_encounters));
fileWatcher.addPath(wildMonJsonFilepath);
@@ -2412,17 +2440,6 @@ bool Project::readObjEventGfxConstants() {
}
bool Project::readMiscellaneousConstants() {
- miscConstants.clear();
- if (userConfig.useEncounterJson) {
- const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_pokemon);
- const QString minLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_min_level);
- const QString maxLevelName = projectConfig.getIdentifier(ProjectIdentifier::define_max_level);
- fileWatcher.addPath(root + "/" + filename);
- QMap pokemonDefines = parser.readCDefinesByName(filename, {minLevelName, maxLevelName});
- miscConstants.insert("max_level_define", pokemonDefines.value(maxLevelName) > pokemonDefines.value(minLevelName) ? pokemonDefines.value(maxLevelName) : 100);
- miscConstants.insert("min_level_define", pokemonDefines.value(minLevelName) < pokemonDefines.value(maxLevelName) ? pokemonDefines.value(minLevelName) : 1);
- }
-
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_global);
const QString maxObjectEventsName = projectConfig.getIdentifier(ProjectIdentifier::define_obj_event_count);
fileWatcher.addPath(root + "/" + filename);
@@ -2611,7 +2628,7 @@ bool Project::readSpeciesIconPaths() {
const QMap iconIncbins = parser.readCIncbinMulti(incfilename);
// Read species constants. If this fails we can get them from the icon table (but we shouldn't rely on it).
- const QStringList regexList = {projectConfig.getIdentifier(ProjectIdentifier::regex_species)};
+ const QStringList regexList = {QString("\\b%1").arg(projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix))};
const QString constantsFilename = projectConfig.getFilePath(ProjectFilePath::constants_species);
fileWatcher.addPath(root + "/" + constantsFilename);
QStringList speciesNames = parser.readCDefineNames(constantsFilename, regexList);
diff --git a/src/ui/customscriptseditor.cpp b/src/ui/customscriptseditor.cpp
index 13650053..b7180b92 100644
--- a/src/ui/customscriptseditor.cpp
+++ b/src/ui/customscriptseditor.cpp
@@ -27,7 +27,7 @@ CustomScriptsEditor::CustomScriptsEditor(QWidget *parent) :
connect(ui->button_CreateNewScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::createNewScript);
connect(ui->button_LoadScript, &QAbstractButton::clicked, this, &CustomScriptsEditor::loadScript);
- connect(ui->button_RefreshScripts, &QAbstractButton::clicked, this, &CustomScriptsEditor::refreshScripts);
+ connect(ui->button_RefreshScripts, &QAbstractButton::clicked, this, &CustomScriptsEditor::userRefreshScripts);
connect(ui->buttonBox, &QDialogButtonBox::clicked, this, &CustomScriptsEditor::dialogButtonClicked);
this->initShortcuts();
@@ -57,7 +57,7 @@ void CustomScriptsEditor::initShortcuts() {
shortcut_load->setObjectName("shortcut_load");
shortcut_load->setWhatsThis("Load Script...");
- auto *shortcut_refresh = new Shortcut(QKeySequence(), this, SLOT(refreshScripts()));
+ auto *shortcut_refresh = new Shortcut(QKeySequence(), this, SLOT(userRefreshScripts()));
shortcut_refresh->setObjectName("shortcut_refresh");
shortcut_refresh->setWhatsThis("Refresh Scripts");
@@ -238,14 +238,21 @@ void CustomScriptsEditor::openSelectedScripts() {
this->openScript(item);
}
-void CustomScriptsEditor::refreshScripts() {
+// When the user refreshes the scripts we show a little tooltip as feedback.
+// We don't want this tooltip to display when we refresh programmatically, like when changes are saved.
+void CustomScriptsEditor::userRefreshScripts() {
+ if (refreshScripts())
+ QToolTip::showText(ui->button_RefreshScripts->mapToGlobal(QPoint(0, 0)), "Refreshed!");
+}
+
+bool CustomScriptsEditor::refreshScripts() {
if (this->hasUnsavedChanges) {
if (this->prompt("Scripts have been modified, save changes and reload the script engine?", QMessageBox::Yes) == QMessageBox::No)
- return;
+ return false;
this->save();
}
- QToolTip::showText(ui->button_RefreshScripts->mapToGlobal(QPoint(0, 0)), "Refreshed!");
emit reloadScriptEngine();
+ return true;
}
void CustomScriptsEditor::save() {
@@ -264,6 +271,7 @@ void CustomScriptsEditor::save() {
}
userConfig.setCustomScripts(paths, enabledStates);
+ userConfig.save();
this->hasUnsavedChanges = false;
this->refreshScripts();
}
diff --git a/src/ui/draggablepixmapitem.cpp b/src/ui/draggablepixmapitem.cpp
index 22511643..59b069dd 100644
--- a/src/ui/draggablepixmapitem.cpp
+++ b/src/ui/draggablepixmapitem.cpp
@@ -34,11 +34,26 @@ void DraggablePixmapItem::updatePixmap() {
}
void DraggablePixmapItem::mousePressEvent(QGraphicsSceneMouseEvent *mouse) {
- active = true;
- QPoint pos = Metatile::coordFromPixmapCoord(mouse->scenePos());
- last_x = pos.x();
- last_y = pos.y();
- this->editor->selectMapEvent(this, mouse->modifiers() & Qt::ControlModifier);
+ if (this->active)
+ return;
+ this->active = true;
+ this->lastPos = Metatile::coordFromPixmapCoord(mouse->scenePos());
+
+ bool selectionToggle = mouse->modifiers() & Qt::ControlModifier;
+ if (selectionToggle || !editor->selected_events->contains(this)) {
+ // User is either toggling this selection on/off as part of a group selection,
+ // or they're newly selecting just this item.
+ this->editor->selectMapEvent(this, selectionToggle);
+ } else {
+ // This item is already selected and the user isn't toggling the selection, so there are 4 possibilities:
+ // 1. This is the only selected event, and the selection is pointless.
+ // 2. This is the only selected event, and they want to drag the item around.
+ // 3. There's a group selection, and they want to start a new selection with just this item.
+ // 4. There's a group selection, and they want to drag the group around.
+ // 'selectMapEvent' will immediately clear the rest of the selection, which supports #1-3 but prevents #4.
+ // To support #4 we set the flag below, and we only call 'selectMapEvent' on mouse release if no move occurred.
+ this->releaseSelectionQueued = true;
+ }
this->editor->selectingEvent = true;
}
@@ -57,28 +72,39 @@ void DraggablePixmapItem::moveTo(const QPoint &pos) {
}
void DraggablePixmapItem::mouseMoveEvent(QGraphicsSceneMouseEvent *mouse) {
- if (active) {
- QPoint pos = Metatile::coordFromPixmapCoord(mouse->scenePos());
- if (pos.x() != last_x || pos.y() != last_y) {
- emit this->editor->map_item->hoveredMapMetatileChanged(pos);
- QList selectedEvents;
- if (editor->selected_events->contains(this)) {
- for (DraggablePixmapItem *item : *editor->selected_events) {
- selectedEvents.append(item->event);
- }
- } else {
- selectedEvents.append(this->event);
- }
- editor->map->editHistory.push(new EventMove(selectedEvents, pos.x() - last_x, pos.y() - last_y, currentActionId));
- last_x = pos.x();
- last_y = pos.y();
+ if (!this->active)
+ return;
+
+ QPoint pos = Metatile::coordFromPixmapCoord(mouse->scenePos());
+ if (pos == this->lastPos)
+ return;
+
+ QPoint moveDistance = pos - this->lastPos;
+ this->lastPos = pos;
+ emit this->editor->map_item->hoveredMapMetatileChanged(pos);
+
+ QList selectedEvents;
+ if (editor->selected_events->contains(this)) {
+ for (DraggablePixmapItem *item : *editor->selected_events) {
+ selectedEvents.append(item->event);
}
+ } else {
+ selectedEvents.append(this->event);
}
+ editor->map->editHistory.push(new EventMove(selectedEvents, moveDistance.x(), moveDistance.y(), currentActionId));
+ this->releaseSelectionQueued = false;
}
-void DraggablePixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *) {
- active = false;
+void DraggablePixmapItem::mouseReleaseEvent(QGraphicsSceneMouseEvent *mouse) {
+ if (!this->active)
+ return;
+ this->active = false;
currentActionId++;
+ if (this->releaseSelectionQueued) {
+ this->releaseSelectionQueued = false;
+ if (Metatile::coordFromPixmapCoord(mouse->scenePos()) == this->lastPos)
+ this->editor->selectMapEvent(this);
+ }
}
void DraggablePixmapItem::mouseDoubleClickEvent(QGraphicsSceneMouseEvent *) {
diff --git a/src/ui/encountertabledelegates.cpp b/src/ui/encountertabledelegates.cpp
index 7824087e..230abc7b 100644
--- a/src/ui/encountertabledelegates.cpp
+++ b/src/ui/encountertabledelegates.cpp
@@ -50,7 +50,7 @@ QWidget *SpeciesComboDelegate::createEditor(QWidget *parent, const QStyleOptionV
void SpeciesComboDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const {
QString species = index.data(Qt::EditRole).toString();
NoScrollComboBox *combo = static_cast(editor);
- combo->setCurrentIndex(combo->findText(species));
+ combo->setCurrentText(species);
}
void SpeciesComboDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const {
@@ -75,12 +75,12 @@ QWidget *SpinBoxDelegate::createEditor(QWidget *parent, const QStyleOptionViewIt
int col = index.column();
if (col == EncounterTableModel::ColumnType::MinLevel || col == EncounterTableModel::ColumnType::MaxLevel) {
- editor->setMinimum(this->project->miscConstants.value("min_level_define").toInt());
- editor->setMaximum(this->project->miscConstants.value("max_level_define").toInt());
+ editor->setMinimum(this->project->pokemonMinLevel);
+ editor->setMaximum(this->project->pokemonMaxLevel);
}
else if (col == EncounterTableModel::ColumnType::EncounterRate) {
editor->setMinimum(0);
- editor->setMaximum(180);
+ editor->setMaximum(this->project->maxEncounterRate);
}
return editor;
diff --git a/src/ui/encountertablemodel.cpp b/src/ui/encountertablemodel.cpp
index fd870e40..f7e73996 100644
--- a/src/ui/encountertablemodel.cpp
+++ b/src/ui/encountertablemodel.cpp
@@ -142,45 +142,57 @@ QVariant EncounterTableModel::headerData(int section, Qt::Orientation orientatio
}
bool EncounterTableModel::setData(const QModelIndex &index, const QVariant &value, int role) {
- if (role == Qt::EditRole) {
- if (!checkIndex(index))
- return false;
+ if (role != Qt::EditRole)
+ return false;
+ if (!checkIndex(index))
+ return false;
- int row = index.row();
- int col = index.column();
+ int row = index.row();
+ int col = index.column();
- switch (col) {
- case ColumnType::Species:
- this->monInfo.wildPokemon[row].species = value.toString();
- break;
-
- case ColumnType::MinLevel: {
- int minLevel = value.toInt();
- this->monInfo.wildPokemon[row].minLevel = minLevel;
- if (minLevel > this->monInfo.wildPokemon[row].maxLevel)
- this->monInfo.wildPokemon[row].maxLevel = minLevel;
- break;
+ switch (col) {
+ case ColumnType::Species: {
+ QString species = value.toString();
+ if (this->monInfo.wildPokemon[row].species != species) {
+ this->monInfo.wildPokemon[row].species = species;
+ emit edited();
}
-
- case ColumnType::MaxLevel: {
- int maxLevel = value.toInt();
- this->monInfo.wildPokemon[row].maxLevel = maxLevel;
- if (maxLevel < this->monInfo.wildPokemon[row].minLevel)
- this->monInfo.wildPokemon[row].minLevel = maxLevel;
- break;
- }
-
- case ColumnType::EncounterRate:
- this->monInfo.encounterRate = value.toInt();
- break;
-
- default:
- return false;
- }
- emit edited();
- return true;
+ break;
}
- return false;
+
+ 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);
+ emit edited();
+ }
+ break;
+ }
+
+ 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);
+ emit edited();
+ }
+ break;
+ }
+
+ case ColumnType::EncounterRate: {
+ int encounterRate = value.toInt();
+ if (this->monInfo.encounterRate != encounterRate) {
+ this->monInfo.encounterRate = encounterRate;
+ emit edited();
+ }
+ break;
+ }
+
+ default:
+ return false;
+ }
+ return true;
}
Qt::ItemFlags EncounterTableModel::flags(const QModelIndex &index) const {
@@ -199,7 +211,3 @@ Qt::ItemFlags EncounterTableModel::flags(const QModelIndex &index) const {
}
return flags | QAbstractTableModel::flags(index);
}
-
-WildMonInfo EncounterTableModel::encounterData() {
- return this->monInfo;
-}
diff --git a/src/ui/montabwidget.cpp b/src/ui/montabwidget.cpp
index 6845a02b..74c52dc5 100644
--- a/src/ui/montabwidget.cpp
+++ b/src/ui/montabwidget.cpp
@@ -68,7 +68,7 @@ void MonTabWidget::paste(int index) {
WildMonInfo newInfo = getDefaultMonInfo(this->editor->project->wildMonFields.at(index));
combineEncounters(newInfo, encounterClipboard);
populateTab(index, newInfo);
- emit editor->wildMonDataChanged();
+ emit editor->wildMonTableEdited();
}
void MonTabWidget::actionCopyTab(int index) {
@@ -88,21 +88,19 @@ void MonTabWidget::actionCopyTab(int index) {
}
void MonTabWidget::actionAddDeleteTab(int index) {
+ clearTableAt(index);
if (activeTabs[index]) {
// delete tab
- clearTableAt(index);
deactivateTab(index);
editor->saveEncounterTabData();
- emit editor->wildMonDataChanged();
}
else {
// add tab
- clearTableAt(index);
populateTab(index, getDefaultMonInfo(editor->project->wildMonFields.at(index)));
editor->saveEncounterTabData();
setCurrentIndex(index);
- emit editor->wildMonDataChanged();
}
+ emit editor->wildMonTableEdited();
}
void MonTabWidget::clearTableAt(int tabIndex) {
@@ -130,6 +128,7 @@ void MonTabWidget::populateTab(int tabIndex, WildMonInfo monInfo) {
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);
speciesTable->setItemDelegateForColumn(EncounterTableModel::ColumnType::Species, new SpeciesComboDelegate(editor->project, this));
@@ -158,6 +157,8 @@ void MonTabWidget::populateTab(int tabIndex, WildMonInfo monInfo) {
}
QTableView *MonTabWidget::tableAt(int tabIndex) {
+ if (tabIndex < 0)
+ return nullptr;
return static_cast(this->widget(tabIndex));
}
diff --git a/src/ui/wildmonchart.cpp b/src/ui/wildmonchart.cpp
new file mode 100644
index 00000000..4594e2bd
--- /dev/null
+++ b/src/ui/wildmonchart.cpp
@@ -0,0 +1,452 @@
+#if __has_include()
+#include "wildmonchart.h"
+#include "ui_wildmonchart.h"
+#include "config.h"
+
+static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts");
+
+static const QList> themes = {
+ {"Light", QChart::ChartThemeLight},
+ {"Dark", QChart::ChartThemeDark},
+ {"Blue Cerulean", QChart::ChartThemeBlueCerulean},
+ {"Brown Sand", QChart::ChartThemeBrownSand},
+ {"Blue NCS", QChart::ChartThemeBlueNcs},
+ {"High Contrast", QChart::ChartThemeHighContrast},
+ {"Blue Icy", QChart::ChartThemeBlueIcy},
+ {"Qt", QChart::ChartThemeQt},
+};
+
+WildMonChart::WildMonChart(QWidget *parent, const EncounterTableModel *table) :
+ QWidget(parent),
+ ui(new Ui::WildMonChart)
+{
+ ui->setupUi(this);
+ setAttribute(Qt::WA_DeleteOnClose);
+ setWindowFlags(Qt::Window);
+
+ connect(ui->button_Help, &QAbstractButton::clicked, this, &WildMonChart::showHelpDialog);
+
+ // Changing these settings changes which level distribution chart is shown
+ connect(ui->groupBox_Species, &QGroupBox::clicked, this, &WildMonChart::refreshLevelDistributionChart);
+ connect(ui->comboBox_Species, &QComboBox::currentTextChanged, this, &WildMonChart::refreshLevelDistributionChart);
+ connect(ui->comboBox_Group, &QComboBox::currentTextChanged, this, &WildMonChart::refreshLevelDistributionChart);
+
+ // Set up Theme combo box
+ for (auto i : themes)
+ ui->comboBox_Theme->addItem(i.first, i.second);
+ connect(ui->comboBox_Theme, &QComboBox::currentTextChanged, this, &WildMonChart::updateTheme);
+
+ // User's theme choice is saved in the config
+ int configThemeIndex = ui->comboBox_Theme->findText(porymapConfig.wildMonChartTheme);
+ if (configThemeIndex >= 0) {
+ const QSignalBlocker blocker(ui->comboBox_Theme);
+ ui->comboBox_Theme->setCurrentIndex(configThemeIndex);
+ } else {
+ porymapConfig.wildMonChartTheme = ui->comboBox_Theme->currentText();
+ }
+
+ restoreGeometry(porymapConfig.wildMonChartGeometry);
+
+ setTable(table);
+};
+
+WildMonChart::~WildMonChart() {
+ delete ui;
+};
+
+void WildMonChart::setTable(const EncounterTableModel *table) {
+ if (this->table == table)
+ return;
+ this->table = table;
+ refresh();
+}
+
+void WildMonChart::clearTable() {
+ setTable(nullptr);
+}
+
+void WildMonChart::clearTableData() {
+ this->groupNames.clear();
+ this->groupNamesReversed.clear();
+ this->speciesInLegendOrder.clear();
+ this->tableIndexToGroupName.clear();
+ this->groupedLevelRanges.clear();
+ this->speciesToGroupedData.clear();
+ this->speciesToColor.clear();
+ setWindowTitle(baseWindowTitle);
+
+ const QSignalBlocker blocker1(ui->comboBox_Species);
+ const QSignalBlocker blocker2(ui->comboBox_Group);
+ ui->comboBox_Species->clear();
+ ui->comboBox_Group->clear();
+ ui->comboBox_Group->setEnabled(false);
+}
+
+// Extract all the data from the table that we need for the charts
+void WildMonChart::readTable() {
+ clearTableData();
+ if (!this->table)
+ return;
+
+ setWindowTitle(QString("%1 - %2").arg(baseWindowTitle).arg(this->table->encounterField().name));
+
+ // Read data about encounter groups, e.g. for "fishing_mons" we want to know table indexes 2-4 belong to "good_rod"
+ for (auto groupPair : this->table->encounterField().groups) {
+ // Frustratingly when adding categories to the charts they insert bottom-to-top, but the table and combo box
+ // insert top-to-bottom. To keep the order visually the same across displays we keep separate ordered lists.
+ this->groupNames.append(groupPair.first);
+ this->groupNamesReversed.prepend(groupPair.first);
+ for (auto i : groupPair.second)
+ this->tableIndexToGroupName.insert(i, groupPair.first);
+ }
+ // Implicitly 1 unnamed group when none are listed
+ if (this->groupNames.isEmpty()) {
+ this->groupNames.append(QString());
+ this->groupNamesReversed.append(QString());
+ }
+
+ // Read data from the table, combining data for duplicate entries
+ const QList tableFrequencies = this->table->percentages();
+ const QVector tablePokemon = this->table->encounterData().wildPokemon;
+ const int numRows = qMin(tableFrequencies.length(), tablePokemon.length());
+ const QString speciesPrefix = projectConfig.getIdentifier(ProjectIdentifier::define_species_prefix);
+ for (int i = 0; i < numRows; i++) {
+ const double frequency = tableFrequencies.at(i);
+ const WildPokemon pokemon = tablePokemon.at(i);
+ const QString groupName = this->tableIndexToGroupName.value(i);
+
+ // Create species label (strip 'SPECIES_' prefix).
+ QString label = pokemon.species;
+ if (label.startsWith(speciesPrefix))
+ label.remove(0, speciesPrefix.length());
+
+ // Add species/level frequency data
+ Summary *summary = &this->speciesToGroupedData[label][groupName];
+ summary->speciesFrequency += frequency;
+ if (pokemon.minLevel > pokemon.maxLevel)
+ continue; // Invalid
+ int numLevels = pokemon.maxLevel - pokemon.minLevel + 1;
+ for (int level = pokemon.minLevel; level <= pokemon.maxLevel; level++)
+ summary->levelFrequencies[level] += frequency / numLevels;
+
+ // Update level min/max for showing level distribution across a group
+ if (!this->groupedLevelRanges.contains(groupName)) {
+ LevelRange *levelRange = &this->groupedLevelRanges[groupName];
+ levelRange->min = pokemon.minLevel;
+ levelRange->max = pokemon.maxLevel;
+ } else {
+ LevelRange *levelRange = &this->groupedLevelRanges[groupName];
+ if (levelRange->min > pokemon.minLevel)
+ levelRange->min = pokemon.minLevel;
+ if (levelRange->max < pokemon.maxLevel)
+ levelRange->max = pokemon.maxLevel;
+ }
+ }
+
+ // Populate combo boxes
+ const QSignalBlocker blocker1(ui->comboBox_Species);
+ const QSignalBlocker blocker2(ui->comboBox_Group);
+ ui->comboBox_Species->clear();
+ ui->comboBox_Species->addItems(getSpeciesNamesAlphabetical());
+ ui->comboBox_Group->clear();
+ ui->comboBox_Group->addItems(this->groupNames);
+ ui->comboBox_Group->setEnabled(usesGroupLabels());
+}
+
+void WildMonChart::refresh() {
+ const QSignalBlocker blocker1(ui->comboBox_Species);
+ const QSignalBlocker blocker2(ui->comboBox_Group);
+ const QString oldSpecies = ui->comboBox_Species->currentText();
+ const QString oldGroup = ui->comboBox_Group->currentText();
+
+ readTable();
+
+ // If the old UI settings are still valid we should restore them
+ int index = ui->comboBox_Species->findText(oldSpecies);
+ if (index >= 0) ui->comboBox_Species->setCurrentIndex(index);
+
+ index = ui->comboBox_Group->findText(oldGroup);
+ if (index >= 0) ui->comboBox_Group->setCurrentIndex(index);
+
+ refreshSpeciesDistributionChart();
+ refreshLevelDistributionChart();
+}
+
+void WildMonChart::refreshSpeciesDistributionChart() {
+ if (ui->chartView_SpeciesDistribution->chart())
+ ui->chartView_SpeciesDistribution->chart()->deleteLater();
+ ui->chartView_SpeciesDistribution->setChart(createSpeciesDistributionChart());
+ limitChartAnimation(ui->chartView_SpeciesDistribution->chart());
+}
+
+void WildMonChart::refreshLevelDistributionChart() {
+ if (ui->chartView_LevelDistribution->chart())
+ ui->chartView_LevelDistribution->chart()->deleteLater();
+ ui->chartView_LevelDistribution->setChart(createLevelDistributionChart());
+ limitChartAnimation(ui->chartView_LevelDistribution->chart());
+}
+
+QStringList WildMonChart::getSpeciesNamesAlphabetical() const {
+ return this->speciesToGroupedData.keys();
+}
+
+double WildMonChart::getSpeciesFrequency(const QString &species, const QString &groupName) const {
+ return this->speciesToGroupedData[species][groupName].speciesFrequency;
+}
+
+QMap WildMonChart::getLevelFrequencies(const QString &species, const QString &groupName) const {
+ return this->speciesToGroupedData[species][groupName].levelFrequencies;
+}
+
+WildMonChart::LevelRange WildMonChart::getLevelRange(const QString &species, const QString &groupName) const {
+ const QList levels = getLevelFrequencies(species, groupName).keys();
+
+ LevelRange range;
+ if (levels.isEmpty()) {
+ range.min = 0;
+ range.max = 0;
+ } else {
+ range.min = levels.first();
+ range.max = levels.last();
+ }
+ return range;
+}
+
+bool WildMonChart::usesGroupLabels() const {
+ return this->groupNames.length() > 1;
+}
+
+QChart* WildMonChart::createSpeciesDistributionChart() {
+ QList barSets;
+ for (const auto species : getSpeciesNamesAlphabetical()) {
+ // Add encounter chance data
+ auto set = new QBarSet(species);
+ for (auto groupName : this->groupNamesReversed)
+ set->append(getSpeciesFrequency(species, groupName) * 100);
+
+ // We order the bar sets from lowest to highest total, left-to-right.
+ for (int i = 0; i < barSets.length() + 1; i++){
+ if (i >= barSets.length() || barSets.at(i)->sum() > set->sum()) {
+ barSets.insert(i, set);
+ break;
+ }
+ }
+
+ // Show species name and % when hovering over a bar set. This covers some shortfalls in our ability to control the chart design
+ // (i.e. bar segments may be too narrow to see the % label, or colors may be hard to match to the legend).
+ connect(set, &QBarSet::hovered, [set] (bool on, int i) {
+ QString text = on ? QString("%1 (%2%)").arg(set->label()).arg(set->at(i)) : "";
+ QToolTip::showText(QCursor::pos(), text);
+ });
+ }
+
+ // Preserve the order we set earlier so that the legend isn't shuffling around for the other all-species charts.
+ this->speciesInLegendOrder.clear();
+ for (auto set : barSets)
+ this->speciesInLegendOrder.append(set->label());
+
+ // Set up series
+ auto series = new QHorizontalPercentBarSeries();
+ series->setLabelsVisible();
+ series->append(barSets);
+
+ // Set up chart
+ auto chart = new QChart();
+ chart->addSeries(series);
+ chart->setTheme(currentTheme());
+ chart->setAnimationOptions(QChart::SeriesAnimations);
+ chart->legend()->setVisible(true);
+ chart->legend()->setShowToolTips(true);
+ chart->legend()->setAlignment(Qt::AlignBottom);
+ saveSpeciesColors(barSets);
+
+ // X-axis is the % frequency. We're already showing percentages on the bar, so we just display 0/50/100%
+ auto axisX = new QValueAxis();
+ axisX->setLabelFormat("%u%%");
+ axisX->setTickCount(3);
+ chart->addAxis(axisX, Qt::AlignBottom);
+ series->attachAxis(axisX);
+
+ // Y-axis is the names of encounter groups (e.g. Old Rod, Good Rod...)
+ if (usesGroupLabels()) {
+ auto axisY = new QBarCategoryAxis();
+ axisY->setCategories(this->groupNamesReversed);
+ chart->addAxis(axisY, Qt::AlignLeft);
+ series->attachAxis(axisY);
+ }
+
+ return chart;
+}
+
+QBarSet* WildMonChart::createLevelDistributionBarSet(const QString &species, const QString &groupName, bool individual) {
+ const double totalFrequency = individual ? getSpeciesFrequency(species, groupName) : 1.0;
+ const QMap levelFrequencies = getLevelFrequencies(species, groupName);
+
+ auto set = new QBarSet(species);
+ LevelRange levelRange = individual ? getLevelRange(species, groupName) : this->groupedLevelRanges.value(groupName);
+ for (int i = levelRange.min; i <= levelRange.max; i++) {
+ set->append(levelFrequencies.value(i, 0) / totalFrequency * 100);
+ }
+
+ // Show data when hovering over a bar set. This covers some shortfalls in our ability to control the chart design.
+ connect(set, &QBarSet::hovered, [=] (bool on, int i) {
+ QString text = on ? QString("%1 Lv%2 (%3%)")
+ .arg(individual ? "" : set->label())
+ .arg(QString::number(i + levelRange.min))
+ .arg(set->at(i))
+ : "";
+ QToolTip::showText(QCursor::pos(), text);
+ });
+
+ // Clicking on a bar set in the stacked chart opens its individual chart
+ if (!individual) {
+ connect(set, &QBarSet::clicked, [this, species](int) {
+ const QSignalBlocker blocker1(ui->groupBox_Species);
+ const QSignalBlocker blocker2(ui->comboBox_Species);
+ ui->groupBox_Species->setChecked(true);
+ ui->comboBox_Species->setCurrentText(species);
+ refreshLevelDistributionChart();
+ });
+ }
+
+ return set;
+}
+
+QChart* WildMonChart::createLevelDistributionChart() {
+ const QString groupName = ui->comboBox_Group->currentText();
+
+ LevelRange levelRange;
+ QList barSets;
+
+ // Create bar sets
+ if (ui->groupBox_Species->isChecked()) {
+ // Species box is active, we just display data for the selected species.
+ const QString species = ui->comboBox_Species->currentText();
+ barSets.append(createLevelDistributionBarSet(species, groupName, true));
+ levelRange = getLevelRange(species, groupName);
+ } else {
+ // Species box is inactive, we display data for all species in the table.
+ for (const auto species : this->speciesInLegendOrder)
+ barSets.append(createLevelDistributionBarSet(species, groupName, false));
+ levelRange = this->groupedLevelRanges.value(groupName);
+ }
+
+ // Set up series
+ auto series = new QStackedBarSeries();
+ //series->setLabelsVisible();
+ series->append(barSets);
+
+ // Set up chart
+ auto chart = new QChart();
+ chart->addSeries(series);
+ chart->setTheme(currentTheme());
+ chart->setAnimationOptions(QChart::SeriesAnimations);
+ chart->legend()->setVisible(true);
+ chart->legend()->setShowToolTips(true);
+ chart->legend()->setAlignment(Qt::AlignBottom);
+ applySpeciesColors(barSets); // Has to happen after theme is set
+
+ // X-axis is the level range.
+ QBarCategoryAxis *axisX = new QBarCategoryAxis();
+ for (int i = levelRange.min; i <= levelRange.max; i++)
+ axisX->append(QString::number(i));
+ chart->addAxis(axisX, Qt::AlignBottom);
+ series->attachAxis(axisX);
+
+ // Y-axis is the % frequency
+ QValueAxis *axisY = new QValueAxis();
+ axisY->setLabelFormat("%u%%");
+ chart->addAxis(axisY, Qt::AlignLeft);
+ series->attachAxis(axisY);
+
+ // We round the y-axis max up to a multiple of 5.
+ auto roundUp = [](int num, int multiple) {
+ auto remainder = num % multiple;
+ if (remainder == 0)
+ return num;
+ return num + multiple - remainder;
+ };
+ axisY->setMax(roundUp(qCeil(axisY->max()), 5));
+
+ return chart;
+}
+
+QChart::ChartTheme WildMonChart::currentTheme() const {
+ return static_cast(ui->comboBox_Theme->currentData().toInt());
+}
+
+void WildMonChart::updateTheme() {
+ auto theme = currentTheme();
+ porymapConfig.wildMonChartTheme = ui->comboBox_Theme->currentText();
+
+ // In order to keep the color of each species in the legend consistent across
+ // charts we save species->color mappings. The legend colors are overwritten
+ // when we change themes, so we need to recalculate them. We let the species
+ // distribution chart determine what those mapping are (it always includes every
+ // species in the table) and then we apply those mappings to subsequent charts.
+ QChart *chart = ui->chartView_SpeciesDistribution->chart();
+ if (!chart)
+ return;
+ chart->setTheme(theme);
+ saveSpeciesColors(static_cast(chart->series().at(0))->barSets());
+
+ chart = ui->chartView_LevelDistribution->chart();
+ if (chart) {
+ chart->setTheme(theme);
+ applySpeciesColors(static_cast(chart->series().at(0))->barSets());
+ }
+}
+
+void WildMonChart::saveSpeciesColors(const QList &barSets) {
+ this->speciesToColor.clear();
+ for (auto set : barSets)
+ this->speciesToColor.insert(set->label(), set->color());
+}
+
+void WildMonChart::applySpeciesColors(const QList &barSets) {
+ for (auto set : barSets)
+ set->setColor(this->speciesToColor.value(set->label()));
+}
+
+// Turn off the animation once it's played, otherwise it replays any time the window changes size.
+void WildMonChart::limitChartAnimation(QChart *chart) {
+ // Chart may be destroyed before the animation finishes
+ QPointer safeChart = chart;
+
+ // QChart has no signal for when the animation is finished, so we use a timer to stop the animation.
+ // It is technically possible to get the chart to freeze mid-animation by resizing the window after
+ // the timer starts but before it finishes, but 1. animations are short so this is difficult to do,
+ // and 2. the issue resolves if the window is resized afterwards, so it's probably fine.
+ QTimer::singleShot(chart->animationDuration() + 500, [safeChart] {
+ if (safeChart) safeChart->setAnimationOptions(QChart::NoAnimation);
+ });
+}
+
+void WildMonChart::showHelpDialog() {
+ static const QString text = "This window provides some visualizations of the data in your current Wild Pokémon tab";
+ static const QString informative =
+ "The Species Distribution tab shows the cumulative encounter chance for each species "
+ "in the table. In other words, it answers the question \"What is the likelihood of encountering "
+ "each species in a single encounter?\""
+ "
"
+ "The Level Distribution tab shows the chance of encountering each species at a particular level. "
+ "In the top left under Group you can select which encounter group to show data for. "
+ "In the top right under Species you can select which species to show data for. "
+ "
"
+ "Individual Mode on the Level Distribution tab toggles whether data is shown for all species in the table. "
+ "The percentages will update to reflect whether you're showing all species or just that individual species. "
+ "In other words, while Individual Mode is checked the chart is answering the question \"If a species x "
+ "is encountered, what is the likelihood that it will be level y\", and while Individual Mode is not checked, "
+ "it answers the question \"For a single encounter, what is the likelihood of encountering a species x at level y.\"";
+ QMessageBox msgBox(QMessageBox::Information, "porymap", text, QMessageBox::Close, this);
+ msgBox.setTextFormat(Qt::RichText);
+ msgBox.setInformativeText(informative);
+ msgBox.exec();
+}
+
+void WildMonChart::closeEvent(QCloseEvent *event) {
+ porymapConfig.wildMonChartGeometry = saveGeometry();
+ QWidget::closeEvent(event);
+}
+
+#endif // __has_include()