Support custom collision graphics

This commit is contained in:
GriffinR 2023-12-06 16:01:02 -05:00
parent 1b9b980121
commit d5210cf230
12 changed files with 157 additions and 38 deletions

View file

@ -97,7 +97,7 @@ Fill Metatile
Field name: ``new_map_metatile`` Field name: ``new_map_metatile``
Elevation Elevation
This is the elevation that will be used to fill new maps. New maps will be filled with passable collision. This is the elevation that will be used to fill new maps. It will also be the default selection on the Collision tab when a map is opened. New maps will be filled with passable collision.
Defaults to ``3``. Defaults to ``3``.

View file

@ -38,8 +38,8 @@ protected:
virtual void onNewConfigFileCreated() = 0; virtual void onNewConfigFileCreated() = 0;
virtual void setUnreadKeys() = 0; virtual void setUnreadKeys() = 0;
bool getConfigBool(QString key, QString value); bool getConfigBool(QString key, QString value);
int getConfigInteger(QString key, QString value, int min, int max, int defaultValue); int getConfigInteger(QString key, QString value, int min = INT_MIN, int max = INT_MAX, int defaultValue = 0);
uint32_t getConfigUint32(QString key, QString value, uint32_t min, uint32_t max, uint32_t defaultValue); uint32_t getConfigUint32(QString key, QString value, uint32_t min = 0, uint32_t max = UINT_MAX, uint32_t defaultValue = 0);
private: private:
bool saveDisabled = false; bool saveDisabled = false;
}; };
@ -227,6 +227,10 @@ public:
this->tilesetsHaveCallback = true; this->tilesetsHaveCallback = true;
this->tilesetsHaveIsCompressed = true; this->tilesetsHaveIsCompressed = true;
this->filePaths.clear(); this->filePaths.clear();
this->eventIconPaths.clear();
this->collisionSheetPath = QString();
this->collisionSheetWidth = 2;
this->collisionSheetHeight = 16;
this->readKeys.clear(); this->readKeys.clear();
} }
static const QMap<ProjectFilePath, std::pair<QString, QString>> defaultPaths; static const QMap<ProjectFilePath, std::pair<QString, QString>> defaultPaths;
@ -299,8 +303,14 @@ public:
void setMapAllowFlagsEnabled(bool enabled); void setMapAllowFlagsEnabled(bool enabled);
void setEventIconPath(Event::Group group, const QString &path); void setEventIconPath(Event::Group group, const QString &path);
QString getEventIconPath(Event::Group group); QString getEventIconPath(Event::Group group);
void setCollisionMapPath(const QString &path); void setCollisionIconPath(int collision, const QString &path);
QString getCollisionMapPath(); QString getCollisionIconPath(int collision);
void setCollisionSheetPath(const QString &path);
QString getCollisionSheetPath();
void setCollisionSheetWidth(int width);
int getCollisionSheetWidth();
void setCollisionSheetHeight(int height);
int getCollisionSheetHeight();
protected: protected:
virtual QString getConfigFilepath() override; virtual QString getConfigFilepath() override;
@ -340,7 +350,9 @@ private:
uint32_t metatileLayerTypeMask; uint32_t metatileLayerTypeMask;
bool enableMapAllowFlags; bool enableMapAllowFlags;
QMap<Event::Group, QString> eventIconPaths; QMap<Event::Group, QString> eventIconPaths;
QString collisionMapPath; QString collisionSheetPath;
int collisionSheetWidth;
int collisionSheetHeight;
}; };
extern ProjectConfig projectConfig; extern ProjectConfig projectConfig;

View file

@ -178,7 +178,7 @@ public:
static QString eventGroupToString(Event::Group group); static QString eventGroupToString(Event::Group group);
static QString eventTypeToString(Event::Type type); static QString eventTypeToString(Event::Type type);
static Event::Type eventTypeFromString(QString type); static Event::Type eventTypeFromString(QString type);
static void initIcons(); static void setIcons();
// protected attributes // protected attributes
protected: protected:
@ -260,8 +260,6 @@ public:
void setFrameFromMovement(QString movement); void setFrameFromMovement(QString movement);
void setPixmapFromSpritesheet(QImage, int, int, bool); void setPixmapFromSpritesheet(QImage, int, int, bool);
static const QPixmap * defaultIcon;
protected: protected:
QString gfx; QString gfx;
@ -347,8 +345,6 @@ public:
void setDestinationWarpID(QString newDestinationWarpID) { this->destinationWarpID = newDestinationWarpID; } void setDestinationWarpID(QString newDestinationWarpID) { this->destinationWarpID = newDestinationWarpID; }
QString getDestinationWarpID() { return this->destinationWarpID; } QString getDestinationWarpID() { return this->destinationWarpID; }
static const QPixmap * defaultIcon;
private: private:
QString destinationMap; QString destinationMap;
QString destinationWarpID; QString destinationWarpID;
@ -375,8 +371,6 @@ public:
virtual void setDefaultValues(Project *project) override = 0; virtual void setDefaultValues(Project *project) override = 0;
virtual QSet<QString> getExpectedFields() override = 0; virtual QSet<QString> getExpectedFields() override = 0;
static const QPixmap * defaultIcon;
}; };
@ -476,8 +470,6 @@ public:
virtual void setDefaultValues(Project *project) override = 0; virtual void setDefaultValues(Project *project) override = 0;
virtual QSet<QString> getExpectedFields() override = 0; virtual QSet<QString> getExpectedFields() override = 0;
static const QPixmap * defaultIcon;
}; };
@ -633,8 +625,6 @@ public:
void setRespawnNPC(uint8_t newRespawnNPC) { this->respawnNPC = newRespawnNPC; } void setRespawnNPC(uint8_t newRespawnNPC) { this->respawnNPC = newRespawnNPC; }
uint8_t getRespawnNPC() { return this->respawnNPC; } uint8_t getRespawnNPC() { return this->respawnNPC; }
static const QPixmap * defaultIcon;
private: private:
int index = -1; int index = -1;
QString locationName; QString locationName;

View file

@ -138,6 +138,7 @@ public:
int scaleIndex = 2; int scaleIndex = 2;
qreal collisionOpacity = 0.5; qreal collisionOpacity = 0.5;
static QList<QList<const QImage*>> collisionIcons;
void objectsView_onMousePress(QMouseEvent *event); void objectsView_onMousePress(QMouseEvent *event);
@ -151,6 +152,7 @@ public:
void scaleMapView(int); void scaleMapView(int);
static void openInTextEditor(const QString &path, int lineNum = 0); static void openInTextEditor(const QString &path, int lineNum = 0);
bool eventLimitReached(Event::Type type); bool eventLimitReached(Event::Type type);
static void setCollisionGraphics();
public slots: public slots:
void openMapScripts() const; void openMapScripts() const;

View file

@ -6,8 +6,8 @@
class MovementPermissionsSelector: public SelectablePixmapItem { class MovementPermissionsSelector: public SelectablePixmapItem {
Q_OBJECT Q_OBJECT
public: public:
MovementPermissionsSelector(QPixmap basePixmap) : MovementPermissionsSelector(int cellWidth, int cellHeight, QPixmap basePixmap) :
SelectablePixmapItem(32, 32, 1, 1), SelectablePixmapItem(cellWidth, cellHeight, 1, 1),
basePixmap(basePixmap) { basePixmap(basePixmap) {
setAcceptHoverEvents(true); setAcceptHoverEvents(true);
} }

View file

@ -61,6 +61,7 @@
<file>icons/ui/midnight_branch_more.png</file> <file>icons/ui/midnight_branch_more.png</file>
<file>images/blank_tileset.png</file> <file>images/blank_tileset.png</file>
<file>images/collisions.png</file> <file>images/collisions.png</file>
<file>images/collisions_unknown.png</file>
<file>images/Entities_16x16.png</file> <file>images/Entities_16x16.png</file>
<file>icons/clipboard.ico</file> <file>icons/clipboard.ico</file>
</qresource> </qresource>

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

View file

@ -628,8 +628,10 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
} else if (key == "enable_triple_layer_metatiles") { } else if (key == "enable_triple_layer_metatiles") {
this->enableTripleLayerMetatiles = getConfigBool(key, value); this->enableTripleLayerMetatiles = getConfigBool(key, value);
} else if (key == "new_map_metatile") { } else if (key == "new_map_metatile") {
// TODO: Update max
this->newMapMetatileId = getConfigUint32(key, value, 0, 1023, 0); this->newMapMetatileId = getConfigUint32(key, value, 0, 1023, 0);
} else if (key == "new_map_elevation") { } else if (key == "new_map_elevation") {
// TODO: Update max
this->newMapElevation = getConfigInteger(key, value, 0, 15, 3); this->newMapElevation = getConfigInteger(key, value, 0, 15, 3);
} else if (key == "new_map_border_metatiles") { } else if (key == "new_map_border_metatiles") {
this->newMapBorderMetatileIds.clear(); this->newMapBorderMetatileIds.clear();
@ -652,13 +654,13 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
} }
this->metatileAttributesSize = size; this->metatileAttributesSize = size;
} else if (key == "metatile_behavior_mask") { } else if (key == "metatile_behavior_mask") {
this->metatileBehaviorMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0); this->metatileBehaviorMask = getConfigUint32(key, value);
} else if (key == "metatile_terrain_type_mask") { } else if (key == "metatile_terrain_type_mask") {
this->metatileTerrainTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0); this->metatileTerrainTypeMask = getConfigUint32(key, value);
} else if (key == "metatile_encounter_type_mask") { } else if (key == "metatile_encounter_type_mask") {
this->metatileEncounterTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0); this->metatileEncounterTypeMask = getConfigUint32(key, value);
} else if (key == "metatile_layer_type_mask") { } else if (key == "metatile_layer_type_mask") {
this->metatileLayerTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0); this->metatileLayerTypeMask = getConfigUint32(key, value);
} else if (key == "enable_map_allow_flags") { } else if (key == "enable_map_allow_flags") {
this->enableMapAllowFlags = getConfigBool(key, value); this->enableMapAllowFlags = getConfigBool(key, value);
#ifdef CONFIG_BACKWARDS_COMPATABILITY #ifdef CONFIG_BACKWARDS_COMPATABILITY
@ -694,6 +696,14 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
this->eventIconPaths[Event::Group::Bg] = value; this->eventIconPaths[Event::Group::Bg] = value;
} else if (key == "event_icon_path_heal") { } else if (key == "event_icon_path_heal") {
this->eventIconPaths[Event::Group::Heal] = value; this->eventIconPaths[Event::Group::Heal] = value;
} else if (key == "collision_sheet_path") {
this->collisionSheetPath = value;
} else if (key == "collision_sheet_width") {
// Max for these two keys is if a user specifies blocks with 1 bit for metatile ID and 15 bits for collision or elevation
// TODO: Test to make sure there shouldn't be a stricter limit given the UI
this->collisionSheetWidth = getConfigInteger(key, value, 1, 0x7FFF, 2);
} else if (key == "collision_sheet_height") {
this->collisionSheetHeight = getConfigInteger(key, value, 1, 0x7FFF, 16);
} else { } else {
logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key)); logWarn(QString("Invalid config key found in config file %1: '%2'").arg(this->getConfigFilepath()).arg(key));
} }
@ -766,6 +776,9 @@ QMap<QString, QString> ProjectConfig::getKeyValueMap() {
map.insert("event_icon_path_coord", this->eventIconPaths[Event::Group::Coord]); map.insert("event_icon_path_coord", this->eventIconPaths[Event::Group::Coord]);
map.insert("event_icon_path_bg", this->eventIconPaths[Event::Group::Bg]); map.insert("event_icon_path_bg", this->eventIconPaths[Event::Group::Bg]);
map.insert("event_icon_path_heal", this->eventIconPaths[Event::Group::Heal]); map.insert("event_icon_path_heal", this->eventIconPaths[Event::Group::Heal]);
map.insert("collision_sheet_path", this->collisionSheetPath);
map.insert("collision_sheet_width", QString::number(this->collisionSheetWidth));
map.insert("collision_sheet_height", QString::number(this->collisionSheetHeight));
return map; return map;
} }
@ -1116,15 +1129,35 @@ QString ProjectConfig::getEventIconPath(Event::Group group) {
return this->eventIconPaths.value(group); return this->eventIconPaths.value(group);
} }
void ProjectConfig::setCollisionMapPath(const QString &path) { // TODO: Expose to project settings editor
this->collisionMapPath = path; void ProjectConfig::setCollisionSheetPath(const QString &path) {
this->collisionSheetPath = path;
this->save(); this->save();
} }
QString ProjectConfig::getCollisionMapPath() { QString ProjectConfig::getCollisionSheetPath() {
return this->collisionMapPath; return this->collisionSheetPath;
} }
void ProjectConfig::setCollisionSheetWidth(int width) {
this->collisionSheetWidth = width;
this->save();
}
int ProjectConfig::getCollisionSheetWidth() {
return this->collisionSheetWidth;
}
void ProjectConfig::setCollisionSheetHeight(int height) {
this->collisionSheetHeight = height;
this->save();
}
int ProjectConfig::getCollisionSheetHeight() {
return this->collisionSheetHeight;
}
UserConfig userConfig; UserConfig userConfig;
QString UserConfig::getConfigFilepath() { QString UserConfig::getConfigFilepath() {

View file

@ -131,7 +131,7 @@ void Event::loadPixmap(Project *) {
this->pixmap = pixmap ? *pixmap : QPixmap(); this->pixmap = pixmap ? *pixmap : QPixmap();
} }
void Event::initIcons() { void Event::setIcons() {
qDeleteAll(icons); qDeleteAll(icons);
icons.clear(); icons.clear();
@ -159,9 +159,9 @@ void Event::initIcons() {
if (customIcon.isNull()) { if (customIcon.isNull()) {
// Custom icon failed to load, use the default icon. // Custom icon failed to load, use the default icon.
icons[group] = new QPixmap(defaultIcons.copy(i * w, 0, w, h)); icons[group] = new QPixmap(defaultIcons.copy(i * w, 0, w, h));
logError(QString("Failed to load custom event icon '%1', using default icon.").arg(customIconPath)); logWarn(QString("Failed to load custom event icon '%1', using default icon.").arg(customIconPath));
} else { } else {
icons[group] = new QPixmap(customIcon); icons[group] = new QPixmap(customIcon.scaled(w, h));
} }
} }
} }

View file

@ -20,6 +20,11 @@
#include <math.h> #include <math.h>
static bool selectNewEvents = false; static bool selectNewEvents = false;
static const QPixmap *collisionSheetPixmap = nullptr;
static const int movementPermissionsSelectorCellSize = 32;
// 2D array mapping collision+elevation combos to an icon.
QList<QList<const QImage*>> Editor::collisionIcons;
Editor::Editor(Ui::MainWindow* ui) Editor::Editor(Ui::MainWindow* ui)
{ {
@ -1484,12 +1489,14 @@ void Editor::displayMovementPermissionSelector() {
scene_collision_metatiles = new QGraphicsScene; scene_collision_metatiles = new QGraphicsScene;
if (!movement_permissions_selector_item) { if (!movement_permissions_selector_item) {
movement_permissions_selector_item = new MovementPermissionsSelector(QPixmap(":/images/collisions.png").scaled(32 * 2, 32 * 16)); // TODO: Don't assume default movement_permissions_selector_item = new MovementPermissionsSelector(movementPermissionsSelectorCellSize,
movementPermissionsSelectorCellSize,
collisionSheetPixmap ? *collisionSheetPixmap : QPixmap());
connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionChanged, connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionChanged,
this, &Editor::onHoveredMovementPermissionChanged); this, &Editor::onHoveredMovementPermissionChanged);
connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionCleared, connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionCleared,
this, &Editor::onHoveredMovementPermissionCleared); this, &Editor::onHoveredMovementPermissionCleared);
movement_permissions_selector_item->select(0, 3); movement_permissions_selector_item->select(0, projectConfig.getNewMapElevation()); // TODO: New map collision config?
} }
scene_collision_metatiles->addItem(movement_permissions_selector_item); scene_collision_metatiles->addItem(movement_permissions_selector_item);
@ -2227,3 +2234,77 @@ void Editor::objectsView_onMousePress(QMouseEvent *event) {
} }
selectingEvent = false; selectingEvent = false;
} }
// TODO: Show elevation & collision spinners on collision tab
// TODO: Hide selection rect for elevation/collision combos not shown on the image
// TODO: Zoom slider
// TODO: Bug--Images with transparency allow users to paint metatiles on the Collision tab
// Custom collision graphics may be provided by the user.
void Editor::setCollisionGraphics() {
static const QImage defaultCollisionImgSheet = QImage(":/images/collisions.png");
QString customPath = projectConfig.getCollisionSheetPath();
QImage imgSheet;
if (!customPath.isEmpty()) {
// Try to load custom collision image
QFileInfo info(customPath);
if (info.isRelative()) {
customPath = QDir::cleanPath(projectConfig.getProjectDir() + QDir::separator() + customPath);
}
imgSheet = QImage(customPath);
if (imgSheet.isNull()) {
// Custom collision image failed to load, use default
logWarn(QString("Failed to load custom collision image '%1', using default.").arg(customPath));
imgSheet = defaultCollisionImgSheet;
}
} else {
// No custom collision image specified, use the default.
imgSheet = defaultCollisionImgSheet;
}
// Like the vanilla collision image, users are not required to provide an image that gives an icon for every elevation/collision combination.
// Instead they tell us how many are provided in their image by specifying the number of columns and rows.
const int imgColumns = projectConfig.getCollisionSheetWidth();
const int imgRows = projectConfig.getCollisionSheetHeight();
// Create a pixmap for the selector on the Collision tab
delete collisionSheetPixmap;
collisionSheetPixmap = new QPixmap(QPixmap::fromImage(imgSheet)
.scaled(movementPermissionsSelectorCellSize * imgColumns,
movementPermissionsSelectorCellSize * imgRows));
for (auto sublist : collisionIcons)
qDeleteAll(sublist);
collisionIcons.clear();
// Chop up the collision image sheet into separate icon images to be displayed on the map.
// Any icons for elevation/collision combinations that aren't provided by the image sheet are also created now.
const int w = 16;
const int h = 16;
const int numCollisions = 4; // TODO: Read value from elsewhere
const int numElevations = 16; // TODO: Read value from elsewhere
imgSheet = imgSheet.scaled(w * imgColumns, h * imgRows);
for (int collision = 0; collision < numCollisions; collision++) {
// If (collision >= imgColumns) here, it's a valid collision value, but it is not represented with an icon on the image sheet.
// In this case we just use the rightmost collision icon. This is mostly to support the vanilla case, where technically 0-3
// are valid collision values, but 1-3 have the same meaning, so the vanilla collision selector image only has 2 columns.
int x = ((collision < imgColumns) ? collision : imgColumns) * w;
QList<const QImage*> sublist;
for (int elevation = 0; elevation < numElevations; elevation++) {
if (elevation < imgRows) {
// This elevation has an icon on the image sheet, add it to the list
int y = elevation * h;
sublist.append(new QImage(imgSheet.copy(x, y, w, h)));
} else {
// This is a valid elevation value, but it has no icon on the image sheet.
// Give it a placeholder "?" icon (white if passable, red otherwise)
static const QImage placeholder = QImage(":/images/collisions_unknown.png");
static const QImage * placeholder_White = new QImage(placeholder.copy(0, 0, w, h));
static const QImage * placeholder_Red = new QImage(placeholder.copy(w, 0, w, h));
sublist.append(x == 0 ? placeholder_White : placeholder_Red);
}
}
collisionIcons.append(sublist);
}
}

View file

@ -391,6 +391,9 @@ void MainWindow::setProjectSpecificUIVisibility()
bool floorNumEnabled = projectConfig.getFloorNumberEnabled(); bool floorNumEnabled = projectConfig.getFloorNumberEnabled();
ui->spinBox_FloorNumber->setVisible(floorNumEnabled); ui->spinBox_FloorNumber->setVisible(floorNumEnabled);
ui->label_FloorNumber->setVisible(floorNumEnabled); ui->label_FloorNumber->setVisible(floorNumEnabled);
Event::setIcons();
Editor::setCollisionGraphics();
} }
void MainWindow::mapSortOrder_changed(QAction *action) void MainWindow::mapSortOrder_changed(QAction *action)
@ -514,7 +517,6 @@ bool MainWindow::openProject(QString dir) {
this->setProjectSpecificUIVisibility(); this->setProjectSpecificUIVisibility();
this->newMapDefaultsSet = false; this->newMapDefaultsSet = false;
Event::initIcons();
Scripting::init(this); Scripting::init(this);
bool already_open = isProjectOpen() && (editor->project->root == dir); bool already_open = isProjectOpen() && (editor->project->root == dir);
if (!already_open) { if (!already_open) {

View file

@ -1,18 +1,16 @@
#include "config.h" #include "config.h"
#include "imageproviders.h" #include "imageproviders.h"
#include "log.h" #include "log.h"
#include "editor.h"
#include <QPainter> #include <QPainter>
QImage getCollisionMetatileImage(Block block) { QImage getCollisionMetatileImage(Block block) {
return getCollisionMetatileImage(block.collision, block.elevation); return getCollisionMetatileImage(block.collision, block.elevation);
} }
// TODO:
QImage getCollisionMetatileImage(int collision, int elevation) { QImage getCollisionMetatileImage(int collision, int elevation) {
static const QImage collisionImage(":/images/collisions.png"); const QImage * image = Editor::collisionIcons.at(collision).at(elevation);
int x = (collision != 0) * 16; return image ? *image : QImage();
int y = elevation * 16;
return collisionImage.copy(x, y, 16, 16);
} }
QImage getMetatileImage( QImage getMetatileImage(