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``
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``.

View file

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

View file

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

View file

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

View file

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

View file

@ -61,6 +61,7 @@
<file>icons/ui/midnight_branch_more.png</file>
<file>images/blank_tileset.png</file>
<file>images/collisions.png</file>
<file>images/collisions_unknown.png</file>
<file>images/Entities_16x16.png</file>
<file>icons/clipboard.ico</file>
</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") {
this->enableTripleLayerMetatiles = getConfigBool(key, value);
} else if (key == "new_map_metatile") {
// TODO: Update max
this->newMapMetatileId = getConfigUint32(key, value, 0, 1023, 0);
} else if (key == "new_map_elevation") {
// TODO: Update max
this->newMapElevation = getConfigInteger(key, value, 0, 15, 3);
} else if (key == "new_map_border_metatiles") {
this->newMapBorderMetatileIds.clear();
@ -652,13 +654,13 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
}
this->metatileAttributesSize = size;
} 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") {
this->metatileTerrainTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0);
this->metatileTerrainTypeMask = getConfigUint32(key, value);
} 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") {
this->metatileLayerTypeMask = getConfigUint32(key, value, 0, 0xFFFFFFFF, 0);
this->metatileLayerTypeMask = getConfigUint32(key, value);
} else if (key == "enable_map_allow_flags") {
this->enableMapAllowFlags = getConfigBool(key, value);
#ifdef CONFIG_BACKWARDS_COMPATABILITY
@ -694,6 +696,14 @@ void ProjectConfig::parseConfigKeyValue(QString key, QString value) {
this->eventIconPaths[Event::Group::Bg] = value;
} else if (key == "event_icon_path_heal") {
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 {
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_bg", this->eventIconPaths[Event::Group::Bg]);
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;
}
@ -1116,15 +1129,35 @@ QString ProjectConfig::getEventIconPath(Event::Group group) {
return this->eventIconPaths.value(group);
}
void ProjectConfig::setCollisionMapPath(const QString &path) {
this->collisionMapPath = path;
// TODO: Expose to project settings editor
void ProjectConfig::setCollisionSheetPath(const QString &path) {
this->collisionSheetPath = path;
this->save();
}
QString ProjectConfig::getCollisionMapPath() {
return this->collisionMapPath;
QString ProjectConfig::getCollisionSheetPath() {
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;
QString UserConfig::getConfigFilepath() {

View file

@ -131,7 +131,7 @@ void Event::loadPixmap(Project *) {
this->pixmap = pixmap ? *pixmap : QPixmap();
}
void Event::initIcons() {
void Event::setIcons() {
qDeleteAll(icons);
icons.clear();
@ -159,9 +159,9 @@ void Event::initIcons() {
if (customIcon.isNull()) {
// Custom icon failed to load, use the default icon.
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 {
icons[group] = new QPixmap(customIcon);
icons[group] = new QPixmap(customIcon.scaled(w, h));
}
}
}

View file

@ -20,6 +20,11 @@
#include <math.h>
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)
{
@ -1484,12 +1489,14 @@ void Editor::displayMovementPermissionSelector() {
scene_collision_metatiles = new QGraphicsScene;
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,
this, &Editor::onHoveredMovementPermissionChanged);
connect(movement_permissions_selector_item, &MovementPermissionsSelector::hoveredMovementPermissionCleared,
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);
@ -2227,3 +2234,77 @@ void Editor::objectsView_onMousePress(QMouseEvent *event) {
}
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();
ui->spinBox_FloorNumber->setVisible(floorNumEnabled);
ui->label_FloorNumber->setVisible(floorNumEnabled);
Event::setIcons();
Editor::setCollisionGraphics();
}
void MainWindow::mapSortOrder_changed(QAction *action)
@ -514,7 +517,6 @@ bool MainWindow::openProject(QString dir) {
this->setProjectSpecificUIVisibility();
this->newMapDefaultsSet = false;
Event::initIcons();
Scripting::init(this);
bool already_open = isProjectOpen() && (editor->project->root == dir);
if (!already_open) {

View file

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