Merge branch 'master' into custom-attr

This commit is contained in:
GriffinR 2024-09-02 23:55:52 -04:00 committed by GitHub
commit 92403b8ab9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 1170 additions and 278 deletions

View file

@ -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

View file

@ -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.
### Changed
@ -17,7 +18,9 @@ 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.
- The triple-layer metatiles setting can now be set automatically using a project constant.
### Fixed
- Fix `Add Region Map...` not updating the region map settings file.
@ -30,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.

View file

@ -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,11 +97,13 @@ 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
``define_pals_primary``, ``NUM_PALS_IN_PRIMARY``,
``define_pals_total``, ``NUM_PALS_TOTAL``,
``define_tiles_per_metatile``, ``NUM_TILES_PER_METATILE``, to determine if triple-layer metatiles are in use. Values other than 8 or 12 are ignored
``define_map_size``, ``MAX_MAP_DATA_SIZE``, to limit map dimensions
``define_mask_metatile``, ``MAPGRID_METATILE_ID_MASK``, optionally read to get settings on ``Maps`` tab
``define_mask_collision``, ``MAPGRID_COLLISION_MASK``, optionally read to get settings on ``Maps`` tab
@ -120,6 +123,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 +138,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

View file

@ -2924,6 +2924,13 @@
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="pushButton_SummaryChart">
<property name="text">
<string>Summary Chart...</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="pushButton_ConfigureEncountersJSON">
<property name="text">

245
forms/wildmonchart.ui Normal file
View file

@ -0,0 +1,245 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>WildMonChart</class>
<widget class="QWidget" name="WildMonChart">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>785</width>
<height>492</height>
</rect>
</property>
<property name="windowTitle">
<string>Wild Pokémon Summary</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QFrame" name="frame_TopBar">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item>
<widget class="QLabel" name="label_Theme">
<property name="text">
<string>Theme</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_Theme"/>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="button_Help">
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../resources/images.qrc">
<normaloff>:/icons/help.ico</normaloff>:/icons/help.ico</iconset>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTabWidget" name="tabWidget">
<property name="currentIndex">
<number>0</number>
</property>
<widget class="QWidget" name="tabSpecies">
<attribute name="title">
<string>Species Distribution</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QChartView" name="chartView_SpeciesDistribution">
<property name="renderHints">
<set>QPainter::Antialiasing</set>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tabLevels">
<attribute name="title">
<string>Level Distribution</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QFrame" name="frame">
<property name="frameShape">
<enum>QFrame::NoFrame</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Plain</enum>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<property name="topMargin">
<number>12</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="label_Group">
<property name="text">
<string>Group</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboBox_Group">
<property name="editable">
<bool>false</bool>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLength</enum>
</property>
<property name="minimumContentsLength">
<number>8</number>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QGroupBox" name="groupBox_Species">
<property name="title">
<string>Individual Mode</string>
</property>
<property name="checkable">
<bool>true</bool>
</property>
<property name="checked">
<bool>false</bool>
</property>
<layout class="QFormLayout" name="formLayout">
<property name="leftMargin">
<number>4</number>
</property>
<property name="topMargin">
<number>4</number>
</property>
<property name="rightMargin">
<number>4</number>
</property>
<property name="bottomMargin">
<number>4</number>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_Species">
<property name="text">
<string>Species</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_Species">
<property name="editable">
<bool>true</bool>
</property>
<property name="insertPolicy">
<enum>QComboBox::NoInsert</enum>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>12</number>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QChartView" name="chartView_LevelDistribution">
<property name="renderHints">
<set>QPainter::Antialiasing</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QChartView</class>
<extends>QGraphicsView</extends>
<header>QtCharts</header>
</customwidget>
</customwidgets>
<resources>
<include location="../resources/images.qrc"/>
</resources>
<connections/>
</ui>

View file

@ -78,6 +78,7 @@ public:
this->monitorFiles = true;
this->tilesetCheckerboardFill = true;
this->theme = "default";
this->wildMonChartTheme = "";
this->textEditorOpenFolder = "";
this->textEditorGotoLine = "";
this->paletteEditorBitDepth = 24;
@ -128,6 +129,7 @@ public:
bool monitorFiles;
bool tilesetCheckerboardFill;
QString theme;
QString wildMonChartTheme;
QString textEditorOpenFolder;
QString textEditorGotoLine;
int paletteEditorBitDepth;
@ -137,6 +139,7 @@ public:
QDateTime lastUpdateCheckTime;
QVersionNumber lastUpdateCheckVersion;
QMap<QUrl, QDateTime> rateLimitTimes;
QByteArray wildMonChartGeometry;
protected:
virtual QString getConfigFilepath() override;
@ -192,11 +195,13 @@ 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,
define_pals_primary,
define_pals_total,
define_tiles_per_metatile,
define_map_size,
define_mask_metatile,
define_mask_collision,
@ -216,6 +221,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,
@ -230,7 +236,6 @@ enum ProjectIdentifier {
regex_sign_facing_directions,
regex_trainer_types,
regex_music,
regex_species,
};
enum ProjectFilePath {
@ -279,6 +284,7 @@ enum ProjectFilePath {
global_fieldmap,
fieldmap,
initial_facing_table,
wild_encounter,
pokemon_icon_table,
pokemon_gfx,
};

View file

@ -118,6 +118,8 @@ public:
}
}
static Event* create(Event::Type type);
static QMap<Event::Group, const QPixmap*> icons;
// standard public methods

View file

@ -22,9 +22,9 @@ struct WildPokemonHeader {
};
struct EncounterField {
QString name;
QString name; // Ex: "fishing_mons"
QVector<int> encounterRates;
tsl::ordered_map<QString, QVector<int>> groups;
tsl::ordered_map<QString, QVector<int>> groups; // Ex: "good_rod", {2, 3, 4}
};
typedef QVector<EncounterField> EncounterFields;

View file

@ -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);
@ -94,8 +96,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 &);

View file

@ -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();
@ -287,6 +287,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);
@ -316,6 +317,7 @@ private:
QPointer<UpdatePromoter> updatePromoter = nullptr;
QPointer<NetworkAccessManager> networkAccessManager = nullptr;
QPointer<AboutPorymap> aboutWindow = nullptr;
QPointer<WildMonChart> wildMonChart = nullptr;
FilterChildrenProxyModel *mapListProxyModel;
QStandardItemModel *mapListModel;
QList<QStandardItem*> *mapGroupItemsList;

View file

@ -39,7 +39,6 @@ public:
QMap<QString, int> mapGroups;
QList<QStringList> groupedMapNames;
QStringList mapNames;
QMap<QString, QVariant> miscConstants;
QList<HealLocation> healLocations;
QMap<QString, int> healLocationNameToValue;
QMap<QString, QString> mapConstantsToMapNames;
@ -79,6 +78,9 @@ public:
bool usingAsmTilesets;
QString importExportPath;
QSet<QString> disabledSettingsNames;
int pokemonMinLevel;
int pokemonMaxLevel;
int maxEncounterRate;
bool wildEncountersLoaded;
void set_root(QString);

View file

@ -55,7 +55,8 @@ private slots:
void dialogButtonClicked(QAbstractButton *button);
void createNewScript();
void loadScript();
void refreshScripts();
bool refreshScripts();
void userRefreshScripts();
void removeSelectedScripts();
void openSelectedScripts();
};

View file

@ -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);

View file

@ -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<double> percentages() const { return this->slotPercentages; }
void resize(int rows, int cols);
private:

View file

@ -61,7 +61,7 @@ private:
void chooseImageFile(QLineEdit * filepathEdit);
void chooseFile(QLineEdit * filepathEdit, const QString &description, const QString &extensions);
QString stripProjectDir(QString s);
void disableParsedSetting(QWidget * widget, const QString &name, const QString &filepath);
bool disableParsedSetting(QWidget * widget, const QString &identifier, const QString &filepath);
void updateMaskOverlapWarning(QLabel * warning, QList<UIntSpinBox*> masks);
QStringList getWarpBehaviorsList();
void setWarpBehaviorsList(QStringList list);

98
include/ui/wildmonchart.h Normal file
View file

@ -0,0 +1,98 @@
#ifndef WILDMONCHART_H
#define WILDMONCHART_H
#include "encountertablemodel.h"
#include <QWidget>
#if __has_include(<QtCharts>)
#include <QtCharts>
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<int, QString> tableIndexToGroupName;
struct LevelRange {
int min;
int max;
};
QMap<QString, LevelRange> groupedLevelRanges;
struct Summary {
double speciesFrequency = 0.0;
QMap<int, double> levelFrequencies;
};
typedef QMap<QString, Summary> GroupedData;
QMap<QString, GroupedData> speciesToGroupedData;
QMap<QString, QColor> speciesToColor;
QStringList getSpeciesNamesAlphabetical() const;
double getSpeciesFrequency(const QString&, const QString&) const;
QMap<int, double> 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<QBarSet*> &);
void applySpeciesColors(const QList<QBarSet*> &);
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(<QtCharts>)
#endif // WILDMONCHART_H

View file

@ -6,6 +6,10 @@
QT += core gui qml network
!win32 {
QT += charts
}
greaterThan(QT_MAJOR_VERSION, 4): QT += widgets
TARGET = porymap
@ -112,7 +116,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 \
@ -213,7 +218,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 \
@ -236,7 +242,8 @@ FORMS += forms/mainwindow.ui \
forms/customscriptseditor.ui \
forms/customscriptslistitem.ui \
forms/customattributesdialog.ui \
forms/updatepromoter.ui
forms/updatepromoter.ui \
forms/wildmonchart.ui
RESOURCES += \
resources/images.qrc \

BIN
resources/icons/help.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -16,6 +16,7 @@
<file>icons/folder_map_opened.ico</file>
<file>icons/folder_map.ico</file>
<file>icons/folder.ico</file>
<file>icons/help.ico</file>
<file>icons/map_edited.ico</file>
<file>icons/map_opened.ico</file>
<file>icons/map.ico</file>

View file

@ -85,11 +85,13 @@ const QMap<ProjectIdentifier, QPair<QString, QString>> 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"}},
{ProjectIdentifier::define_pals_primary, {"define_pals_primary", "NUM_PALS_IN_PRIMARY"}},
{ProjectIdentifier::define_pals_total, {"define_pals_total", "NUM_PALS_TOTAL"}},
{ProjectIdentifier::define_tiles_per_metatile, {"define_tiles_per_metatile", "NUM_TILES_PER_METATILE"}},
{ProjectIdentifier::define_map_size, {"define_map_size", "MAX_MAP_DATA_SIZE"}},
{ProjectIdentifier::define_mask_metatile, {"define_mask_metatile", "MAPGRID_METATILE_ID_MASK"}},
{ProjectIdentifier::define_mask_collision, {"define_mask_collision", "MAPGRID_COLLISION_MASK"}},
@ -109,6 +111,7 @@ const QMap<ProjectIdentifier, QPair<QString, QString>> 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 +127,6 @@ const QMap<ProjectIdentifier, QPair<QString, QString>> 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<ProjectFilePath, QPair<QString, QString>> ProjectConfig::defaultPaths = {
@ -174,6 +176,7 @@ const QMap<ProjectFilePath, QPair<QString, QString>> 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/"}},
};
@ -368,6 +371,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") {
@ -394,6 +399,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") {
@ -453,6 +460,7 @@ QMap<QString, QString> 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));
@ -472,6 +480,7 @@ QMap<QString, QString> 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));

View file

@ -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() {

View file

@ -6,6 +6,21 @@
QMap<Event::Group, const QPixmap*> 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();

View file

@ -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<MonTabWidget*>(ui->stackedWidget_WildMons->currentWidget());
if (!tabWidget) return nullptr;
auto tableView = tabWidget->tableAt(tabWidget->currentIndex());
if (!tableView) return nullptr;
return static_cast<EncounterTableModel*>(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);
}
}
}
@ -1993,10 +2013,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;
@ -2006,10 +2022,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.
@ -2071,56 +2087,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
@ -2227,10 +2211,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;
}

View file

@ -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<int>::of(&QComboBox::currentIndexChanged), [=](int index){
stack->setCurrentIndex(index);
connect(ui->comboBox_EncounterGroupLabel, QOverload<int>::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);
@ -1123,6 +1125,7 @@ void MainWindow::clearProjectUI() {
// Clear map list
mapListModel->clear();
mapListIndexes.clear();
mapGroupItemsList->clear();
}
@ -1139,6 +1142,7 @@ void MainWindow::sortMapList() {
ui->mapList->setUpdatesEnabled(false);
mapListModel->clear();
mapListIndexes.clear();
mapGroupItemsList->clear();
QStandardItem *root = mapListModel->invisibleRootItem();
@ -1771,7 +1775,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") {
@ -1783,56 +1787,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()) {
@ -2024,7 +1997,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");
@ -2572,11 +2545,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();
@ -2669,6 +2637,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);
}
@ -3104,6 +3082,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;

View file

@ -113,6 +113,7 @@ bool Project::sanityCheck() {
}
bool Project::load() {
this->disabledSettingsNames.clear();
bool success = readMapLayouts()
&& readRegionMapSections()
&& readItemNames()
@ -1654,15 +1655,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);
@ -1960,6 +1989,7 @@ bool Project::readFieldmapProperties() {
const QString numPalsPrimaryName = projectConfig.getIdentifier(ProjectIdentifier::define_pals_primary);
const QString numPalsTotalName = projectConfig.getIdentifier(ProjectIdentifier::define_pals_total);
const QString maxMapSizeName = projectConfig.getIdentifier(ProjectIdentifier::define_map_size);
const QString numTilesPerMetatileName = projectConfig.getIdentifier(ProjectIdentifier::define_tiles_per_metatile);
const QStringList names = {
numTilesPrimaryName,
numTilesTotalName,
@ -1967,6 +1997,7 @@ bool Project::readFieldmapProperties() {
numPalsPrimaryName,
numPalsTotalName,
maxMapSizeName,
numTilesPerMetatileName,
};
const QString filename = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap);
fileWatcher.addPath(root + "/" + filename);
@ -2007,6 +2038,22 @@ bool Project::readFieldmapProperties() {
.arg(Project::max_map_data_size));
}
it = defines.find(numTilesPerMetatileName);
if (it != defines.end()) {
// We can determine whether triple-layer metatiles are in-use by reading this constant.
// If the constant is missing (or is using a value other than 8 or 12) the user must tell
// us whether they're using triple-layer metatiles under Project Settings.
static const int numTilesPerLayer = 4;
int numTilesPerMetatile = it.value();
if (numTilesPerMetatile == 2 * numTilesPerLayer) {
projectConfig.tripleLayerMetatilesEnabled = false;
this->disabledSettingsNames.insert(numTilesPerMetatileName);
} else if (numTilesPerMetatile == 3 * numTilesPerLayer) {
projectConfig.tripleLayerMetatilesEnabled = true;
this->disabledSettingsNames.insert(numTilesPerMetatileName);
}
}
return true;
}
@ -2032,7 +2079,8 @@ bool Project::readFieldmapMasks() {
// If users do have the defines we disable them in the settings editor and direct them to their project files.
// Record the names we read so we know later which settings to disable.
const QStringList defineNames = defines.keys();
this->disabledSettingsNames = QSet<QString>(defineNames.constBegin(), defineNames.constEnd());
for (auto name : defineNames)
this->disabledSettingsNames.insert(name);
// Read Block masks
auto readBlockMask = [defines](const QString name, uint16_t *value) {
@ -2412,17 +2460,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<QString, int> 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 +2648,7 @@ bool Project::readSpeciesIconPaths() {
const QMap<QString, QString> 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 prefixes = {projectConfig.getIdentifier(ProjectIdentifier::regex_species)};
const QStringList prefixes = {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, prefixes);

View file

@ -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();
}

View file

@ -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 <Event *> 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 <Event *> 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 *) {

View file

@ -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<NoScrollComboBox *>(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;

View file

@ -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;
}

View file

@ -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<QTableView *>(this->widget(tabIndex));
}

View file

@ -133,53 +133,40 @@ void ProjectSettingsEditor::initUi() {
ui->spinBox_CollisionMask->setMaximum(Block::maxValue);
ui->spinBox_ElevationMask->setMaximum(Block::maxValue);
// Some settings can be determined by constants in the project.
// We reflect that here by disabling their UI elements.
if (project) {
const QString maskFilepath = projectConfig.getFilePath(ProjectFilePath::global_fieldmap);
const QString attrTableFilepath = projectConfig.getFilePath(ProjectFilePath::fieldmap);
const QString metatileIdMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_metatile);
const QString collisionMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_collision);
const QString elevationMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_elevation);
const QString behaviorMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_behavior);
const QString layerTypeMaskName = projectConfig.getIdentifier(ProjectIdentifier::define_mask_layer);
const QString behaviorTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_behavior);
const QString layerTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_layer);
const QString encounterTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_encounter);
const QString terrainTypeTableName = projectConfig.getIdentifier(ProjectIdentifier::define_attribute_terrain);
const QString attrTableName = projectConfig.getIdentifier(ProjectIdentifier::symbol_attribute_table);
// The values for some of the settings we provide in this window can be determined using constants in the user's projects.
// If the user has these constants we disable these settings in the UI -- they can modify them using their constants.
const QString globalFieldmapPath = projectConfig.getFilePath(ProjectFilePath::global_fieldmap);
const QString constantsFieldmapPath = projectConfig.getFilePath(ProjectFilePath::constants_fieldmap);
const QString fieldmapPath = projectConfig.getFilePath(ProjectFilePath::fieldmap);
// Block masks
if (project->disabledSettingsNames.contains(metatileIdMaskName))
this->disableParsedSetting(ui->spinBox_MetatileIdMask, metatileIdMaskName, maskFilepath);
if (project->disabledSettingsNames.contains(collisionMaskName))
this->disableParsedSetting(ui->spinBox_CollisionMask, collisionMaskName, maskFilepath);
if (project->disabledSettingsNames.contains(elevationMaskName))
this->disableParsedSetting(ui->spinBox_ElevationMask, elevationMaskName, maskFilepath);
// Block masks
this->disableParsedSetting(ui->spinBox_MetatileIdMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_metatile), globalFieldmapPath);
this->disableParsedSetting(ui->spinBox_CollisionMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_collision), globalFieldmapPath);
this->disableParsedSetting(ui->spinBox_ElevationMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_elevation), globalFieldmapPath);
// Behavior mask
if (project->disabledSettingsNames.contains(behaviorMaskName))
this->disableParsedSetting(ui->spinBox_BehaviorMask, behaviorMaskName, maskFilepath);
else if (project->disabledSettingsNames.contains(behaviorTableName))
this->disableParsedSetting(ui->spinBox_BehaviorMask, attrTableName, attrTableFilepath);
// Behavior mask
if (!this->disableParsedSetting(ui->spinBox_BehaviorMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_behavior), globalFieldmapPath))
this->disableParsedSetting(ui->spinBox_BehaviorMask, projectConfig.getIdentifier(ProjectIdentifier::define_attribute_behavior), fieldmapPath);
// Layer type mask
if (project->disabledSettingsNames.contains(layerTypeMaskName))
this->disableParsedSetting(ui->spinBox_LayerTypeMask, layerTypeMaskName, maskFilepath);
else if (project->disabledSettingsNames.contains(layerTypeTableName))
this->disableParsedSetting(ui->spinBox_LayerTypeMask, attrTableName, attrTableFilepath);
// Layer type mask
if (!this->disableParsedSetting(ui->spinBox_LayerTypeMask, projectConfig.getIdentifier(ProjectIdentifier::define_mask_layer), globalFieldmapPath))
this->disableParsedSetting(ui->spinBox_LayerTypeMask, projectConfig.getIdentifier(ProjectIdentifier::define_attribute_layer), fieldmapPath);
// Encounter and terrain type masks
if (project->disabledSettingsNames.contains(encounterTypeTableName))
this->disableParsedSetting(ui->spinBox_EncounterTypeMask, attrTableName, attrTableFilepath);
if (project->disabledSettingsNames.contains(terrainTypeTableName))
this->disableParsedSetting(ui->spinBox_TerrainTypeMask, attrTableName, attrTableFilepath);
}
// Encounter and terrain type masks
this->disableParsedSetting(ui->spinBox_EncounterTypeMask, projectConfig.getIdentifier(ProjectIdentifier::define_attribute_encounter), fieldmapPath);
this->disableParsedSetting(ui->spinBox_TerrainTypeMask, projectConfig.getIdentifier(ProjectIdentifier::define_attribute_terrain), fieldmapPath);
// Tripe layer metatiles
this->disableParsedSetting(ui->checkBox_EnableTripleLayerMetatiles, projectConfig.getIdentifier(ProjectIdentifier::define_tiles_per_metatile), constantsFieldmapPath);
}
void ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString &name, const QString &filepath) {
widget->setEnabled(false);
widget->setToolTip(QString("This value has been read from '%1' in %2").arg(name).arg(filepath));
bool ProjectSettingsEditor::disableParsedSetting(QWidget * widget, const QString &identifier, const QString &filepath) {
if (project && project->disabledSettingsNames.contains(identifier)) {
widget->setEnabled(false);
widget->setToolTip(QString("This value has been set using '%1' in %2").arg(identifier).arg(filepath));
return true;
}
return false;
}
// Remember the current settings tab for future sessions

452
src/ui/wildmonchart.cpp Normal file
View file

@ -0,0 +1,452 @@
#if __has_include(<QtCharts>)
#include "wildmonchart.h"
#include "ui_wildmonchart.h"
#include "config.h"
static const QString baseWindowTitle = QString("Wild Pokémon Summary Charts");
static const QList<QPair<QString, QChart::ChartTheme>> 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<double> tableFrequencies = this->table->percentages();
const QVector<WildPokemon> 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<int, double> 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<int> 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<QBarSet*> 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<int, double> 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<QBarSet*> 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<QChart::ChartTheme>(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<QAbstractBarSeries*>(chart->series().at(0))->barSets());
chart = ui->chartView_LevelDistribution->chart();
if (chart) {
chart->setTheme(theme);
applySpeciesColors(static_cast<QAbstractBarSeries*>(chart->series().at(0))->barSets());
}
}
void WildMonChart::saveSpeciesColors(const QList<QBarSet*> &barSets) {
this->speciesToColor.clear();
for (auto set : barSets)
this->speciesToColor.insert(set->label(), set->color());
}
void WildMonChart::applySpeciesColors(const QList<QBarSet*> &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<QChart> 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 <b>Species Distribution</b> 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?\""
"<br><br>"
"The <b>Level Distribution</b> tab shows the chance of encountering each species at a particular level. "
"In the top left under <b>Group</b> you can select which encounter group to show data for. "
"In the top right under <b>Species</b> you can select which species to show data for. "
"<br><br>"
"<b>Individual Mode</b> on the <b>Level Distribution</b> 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 <b>Individual Mode</b> 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 <b>Individual Mode</b> 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(<QtCharts>)