diff --git a/src/configuration/configuration.cpp b/src/configuration/configuration.cpp index 1541e3de1..3000e80d3 100644 --- a/src/configuration/configuration.cpp +++ b/src/configuration/configuration.cpp @@ -7,6 +7,7 @@ #include "configuration.h" #include "../global/utils.h" +#include "../map/infomark.h" #include #include @@ -52,6 +53,25 @@ NODISCARD const char *getPlatformEditor() } } +NODISCARD TextureSetEnum intToTextureSet(int value) +{ + switch (value) { + case 0: + return TextureSetEnum::CLASSIC; + case 1: + return TextureSetEnum::MODERN; + case 2: + return TextureSetEnum::CUSTOM; + default: + return TextureSetEnum::MODERN; // Default to Modern + } +} + +NODISCARD int textureSetToInt(TextureSetEnum value) +{ + return static_cast(value); +} + } // namespace Configuration::Configuration() @@ -194,10 +214,12 @@ ConstString GRP_ACCOUNT = "Account"; ConstString GRP_AUTO_LOAD_WORLD = "Auto load world"; ConstString GRP_AUTO_LOG = "Auto log"; ConstString GRP_CANVAS = "Canvas"; +ConstString GRP_COMMS = "Communications"; ConstString GRP_CONNECTION = "Connection"; ConstString GRP_FINDROOMS_DIALOG = "FindRooms Dialog"; ConstString GRP_GENERAL = "General"; ConstString GRP_GROUP_MANAGER = "Group Manager"; +ConstString GRP_HOTKEYS = "Hotkeys"; ConstString GRP_INFOMARKS_DIALOG = "InfoMarks Dialog"; ConstString GRP_INTEGRATED_MUD_CLIENT = "Integrated Mud Client"; ConstString GRP_MUME_CLIENT_PROTOCOL = "Mume client protocol"; @@ -227,9 +249,12 @@ ConstString KEY_CONNECTION_NORMAL_COLOR = "Connection normal color"; ConstString KEY_CORRECT_POSITION_BONUS = "correct position bonus"; ConstString KEY_DISPLAY_XP_STATUS = "Display XP status bar widget"; ConstString KEY_DISPLAY_CLOCK = "Display clock"; +ConstString KEY_GMCP_BROADCAST_CLOCK = "GMCP broadcast clock"; +ConstString KEY_GMCP_BROADCAST_INTERVAL = "GMCP broadcast interval"; ConstString KEY_DRAW_DOOR_NAMES = "Draw door names"; ConstString KEY_DRAW_NOT_MAPPED_EXITS = "Draw not mapped exits"; ConstString KEY_DRAW_UPPER_LAYERS_TEXTURED = "Draw upper layers textured"; +ConstString KEY_LAYER_TRANSPARENCY = "Layer transparency"; ConstString KEY_EMOJI_ENCODE = "encode emoji"; ConstString KEY_EMOJI_DECODE = "decode emoji"; ConstString KEY_EMULATED_EXITS = "Emulated Exits"; @@ -256,6 +281,73 @@ ConstString KEY_3D_FOV = "canvas.advanced.fov"; ConstString KEY_3D_VERTICAL_ANGLE = "canvas.advanced.verticalAngle"; ConstString KEY_3D_HORIZONTAL_ANGLE = "canvas.advanced.horizontalAngle"; ConstString KEY_3D_LAYER_HEIGHT = "canvas.advanced.layerHeight"; +ConstString KEY_BACKGROUND_IMAGE_ENABLED = "canvas.advanced.backgroundImageEnabled"; +ConstString KEY_BACKGROUND_IMAGE_PATH = "canvas.advanced.backgroundImagePath"; +ConstString KEY_BACKGROUND_IMAGE_FIT_MODE = "canvas.advanced.backgroundFitMode"; +ConstString KEY_BACKGROUND_IMAGE_OPACITY = "canvas.advanced.backgroundOpacity"; +ConstString KEY_BACKGROUND_IMAGE_FOCUSED_SCALE = "canvas.advanced.backgroundFocusedScale"; +ConstString KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_X = "canvas.advanced.backgroundFocusedOffsetX"; +ConstString KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_Y = "canvas.advanced.backgroundFocusedOffsetY"; +ConstString KEY_VISIBLE_MARKER_GENERIC = "canvas.visibleMarkers.generic"; +ConstString KEY_VISIBLE_MARKER_HERB = "canvas.visibleMarkers.herb"; +ConstString KEY_VISIBLE_MARKER_RIVER = "canvas.visibleMarkers.river"; +ConstString KEY_VISIBLE_MARKER_PLACE = "canvas.visibleMarkers.place"; +ConstString KEY_VISIBLE_MARKER_MOB = "canvas.visibleMarkers.mob"; +ConstString KEY_VISIBLE_MARKER_COMMENT = "canvas.visibleMarkers.comment"; +ConstString KEY_VISIBLE_MARKER_ROAD = "canvas.visibleMarkers.road"; +ConstString KEY_VISIBLE_MARKER_OBJECT = "canvas.visibleMarkers.object"; +ConstString KEY_VISIBLE_MARKER_ACTION = "canvas.visibleMarkers.action"; +ConstString KEY_VISIBLE_MARKER_LOCALITY = "canvas.visibleMarkers.locality"; +ConstString KEY_VISIBLE_CONNECTIONS = "canvas.visibilityFilter.connections"; + +// Hotkey configuration keys +ConstString KEY_HOTKEY_FILE_OPEN = "hotkeys.fileOpen"; +ConstString KEY_HOTKEY_FILE_SAVE = "hotkeys.fileSave"; +ConstString KEY_HOTKEY_FILE_RELOAD = "hotkeys.fileReload"; +ConstString KEY_HOTKEY_FILE_QUIT = "hotkeys.fileQuit"; +ConstString KEY_HOTKEY_EDIT_UNDO = "hotkeys.editUndo"; +ConstString KEY_HOTKEY_EDIT_REDO = "hotkeys.editRedo"; +ConstString KEY_HOTKEY_EDIT_PREFERENCES = "hotkeys.editPreferences"; +ConstString KEY_HOTKEY_EDIT_PREFERENCES_ALT = "hotkeys.editPreferencesAlt"; +ConstString KEY_HOTKEY_EDIT_FIND_ROOMS = "hotkeys.editFindRooms"; +ConstString KEY_HOTKEY_EDIT_ROOM = "hotkeys.editRoom"; +ConstString KEY_HOTKEY_VIEW_ZOOM_IN = "hotkeys.viewZoomIn"; +ConstString KEY_HOTKEY_VIEW_ZOOM_OUT = "hotkeys.viewZoomOut"; +ConstString KEY_HOTKEY_VIEW_ZOOM_RESET = "hotkeys.viewZoomReset"; +ConstString KEY_HOTKEY_VIEW_LAYER_UP = "hotkeys.viewLayerUp"; +ConstString KEY_HOTKEY_VIEW_LAYER_DOWN = "hotkeys.viewLayerDown"; +ConstString KEY_HOTKEY_VIEW_LAYER_RESET = "hotkeys.viewLayerReset"; +ConstString KEY_HOTKEY_VIEW_RADIAL_TRANSPARENCY = "hotkeys.viewRadialTransparency"; +ConstString KEY_HOTKEY_VIEW_STATUS_BAR = "hotkeys.viewStatusBar"; +ConstString KEY_HOTKEY_VIEW_SCROLL_BARS = "hotkeys.viewScrollBars"; +ConstString KEY_HOTKEY_VIEW_MENU_BAR = "hotkeys.viewMenuBar"; +ConstString KEY_HOTKEY_VIEW_ALWAYS_ON_TOP = "hotkeys.viewAlwaysOnTop"; +ConstString KEY_HOTKEY_PANEL_LOG = "hotkeys.panelLog"; +ConstString KEY_HOTKEY_PANEL_CLIENT = "hotkeys.panelClient"; +ConstString KEY_HOTKEY_PANEL_GROUP = "hotkeys.panelGroup"; +ConstString KEY_HOTKEY_PANEL_ROOM = "hotkeys.panelRoom"; +ConstString KEY_HOTKEY_PANEL_ADVENTURE = "hotkeys.panelAdventure"; +ConstString KEY_HOTKEY_PANEL_COMMS = "hotkeys.panelComms"; +ConstString KEY_HOTKEY_PANEL_DESCRIPTION = "hotkeys.panelDescription"; +ConstString KEY_HOTKEY_MODE_MOVE_MAP = "hotkeys.modeMoveMap"; +ConstString KEY_HOTKEY_MODE_RAYPICK = "hotkeys.modeRaypick"; +ConstString KEY_HOTKEY_MODE_SELECT_ROOMS = "hotkeys.modeSelectRooms"; +ConstString KEY_HOTKEY_MODE_SELECT_MARKERS = "hotkeys.modeSelectMarkers"; +ConstString KEY_HOTKEY_MODE_SELECT_CONNECTION = "hotkeys.modeSelectConnection"; +ConstString KEY_HOTKEY_MODE_CREATE_MARKER = "hotkeys.modeCreateMarker"; +ConstString KEY_HOTKEY_MODE_CREATE_ROOM = "hotkeys.modeCreateRoom"; +ConstString KEY_HOTKEY_MODE_CREATE_CONNECTION = "hotkeys.modeCreateConnection"; +ConstString KEY_HOTKEY_MODE_CREATE_ONEWAY_CONNECTION = "hotkeys.modeCreateOnewayConnection"; +ConstString KEY_HOTKEY_ROOM_CREATE = "hotkeys.roomCreate"; +ConstString KEY_HOTKEY_ROOM_MOVE_UP = "hotkeys.roomMoveUp"; +ConstString KEY_HOTKEY_ROOM_MOVE_DOWN = "hotkeys.roomMoveDown"; +ConstString KEY_HOTKEY_ROOM_MERGE_UP = "hotkeys.roomMergeUp"; +ConstString KEY_HOTKEY_ROOM_MERGE_DOWN = "hotkeys.roomMergeDown"; +ConstString KEY_HOTKEY_ROOM_DELETE = "hotkeys.roomDelete"; +ConstString KEY_HOTKEY_ROOM_CONNECT_NEIGHBORS = "hotkeys.roomConnectNeighbors"; +ConstString KEY_HOTKEY_ROOM_MOVE_TO_SELECTED = "hotkeys.roomMoveToSelected"; +ConstString KEY_HOTKEY_ROOM_UPDATE_SELECTED = "hotkeys.roomUpdateSelected"; + ConstString KEY_LAST_MAP_LOAD_DIRECTORY = "Last map load directory"; ConstString KEY_LINES_OF_INPUT_HISTORY = "Lines of input history"; ConstString KEY_LINES_OF_PEEK_PREVIEW = "Lines of peek preview"; @@ -270,6 +362,8 @@ ConstString KEY_PROXY_CONNECTION_STATUS = "Proxy connection status"; ConstString KEY_PROXY_LISTENS_ON_ANY_INTERFACE = "Proxy listens on any interface"; ConstString KEY_RELATIVE_PATH_ACCEPTANCE = "relative path acceptance"; ConstString KEY_RESOURCES_DIRECTORY = "canvas.resourcesDir"; +ConstString KEY_TEXTURE_SET = "canvas.textureSet"; +ConstString KEY_ENABLE_SEASONAL_TEXTURES = "canvas.enableSeasonalTextures"; ConstString KEY_MUME_REMOTE_PORT = "Remote port number"; ConstString KEY_REMEMBER_LOGIN = "remember login"; ConstString KEY_ROOM_CREATION_PENALTY = "room creation penalty"; @@ -441,6 +535,8 @@ NODISCARD static uint16_t sanitizeUint16(const int input, const uint16_t default GROUP_CALLBACK(callback, GRP_GENERAL, general); \ GROUP_CALLBACK(callback, GRP_CONNECTION, connection); \ GROUP_CALLBACK(callback, GRP_CANVAS, canvas); \ + GROUP_CALLBACK(callback, GRP_HOTKEYS, hotkeys); \ + GROUP_CALLBACK(callback, GRP_COMMS, comms); \ GROUP_CALLBACK(callback, GRP_ACCOUNT, account); \ GROUP_CALLBACK(callback, GRP_AUTO_LOAD_WORLD, autoLoad); \ GROUP_CALLBACK(callback, GRP_AUTO_LOG, autoLog); \ @@ -565,7 +661,7 @@ void Configuration::ConnectionSettings::read(const QSettings &conf) } // closest well-known color is "Outer Space" -static constexpr const std::string_view DEFAULT_BGCOLOR = "#2E3436"; +static constexpr const std::string_view DEFAULT_BGCOLOR = "#161f21"; // closest well-known color is "Dusty Gray" static constexpr const std::string_view DEFAULT_DARK_COLOR = "#A19494"; // closest well-known color is "Cold Turkey" @@ -587,11 +683,14 @@ void Configuration::CanvasSettings::read(const QSettings &conf) .append(DEFAULT_MMAPPER_SUBDIR) .append(DEFAULT_RESOURCES_SUBDIR)) .toString(); + textureSet = intToTextureSet(conf.value(KEY_TEXTURE_SET, 1).toInt()); // Default: MODERN + enableSeasonalTextures = conf.value(KEY_ENABLE_SEASONAL_TEXTURES, true).toBool(); showMissingMapId.set(conf.value(KEY_SHOW_MISSING_MAP_ID, true).toBool()); showUnsavedChanges.set(conf.value(KEY_SHOW_UNSAVED_CHANGES, true).toBool()); showUnmappedExits.set(conf.value(KEY_DRAW_NOT_MAPPED_EXITS, true).toBool()); drawUpperLayersTextured = conf.value(KEY_DRAW_UPPER_LAYERS_TEXTURED, false).toBool(); drawDoorNames = conf.value(KEY_DRAW_DOOR_NAMES, true).toBool(); + layerTransparency = conf.value(KEY_LAYER_TRANSPARENCY, 1.0).toFloat(); backgroundColor = lookupColor(KEY_BACKGROUND_COLOR, DEFAULT_BGCOLOR); connectionNormalColor = lookupColor(KEY_CONNECTION_NORMAL_COLOR, Colors::white.toHex()); roomDarkColor = lookupColor(KEY_ROOM_DARK_COLOR, DEFAULT_DARK_COLOR); @@ -605,6 +704,30 @@ void Configuration::CanvasSettings::read(const QSettings &conf) advanced.verticalAngle.set(conf.value(KEY_3D_VERTICAL_ANGLE, 450).toInt()); advanced.horizontalAngle.set(conf.value(KEY_3D_HORIZONTAL_ANGLE, 0).toInt()); advanced.layerHeight.set(conf.value(KEY_3D_LAYER_HEIGHT, 15).toInt()); + + // Load background image settings + advanced.useBackgroundImage = conf.value(KEY_BACKGROUND_IMAGE_ENABLED, false).toBool(); + advanced.backgroundImagePath = conf.value(KEY_BACKGROUND_IMAGE_PATH, "").toString(); + advanced.backgroundFitMode = conf.value(KEY_BACKGROUND_IMAGE_FIT_MODE, 0).toInt(); + advanced.backgroundOpacity = conf.value(KEY_BACKGROUND_IMAGE_OPACITY, 1.0f).toFloat(); + advanced.backgroundFocusedScale = conf.value(KEY_BACKGROUND_IMAGE_FOCUSED_SCALE, 1.0f).toFloat(); + advanced.backgroundFocusedOffsetX = conf.value(KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_X, 0.0f) + .toFloat(); + advanced.backgroundFocusedOffsetY = conf.value(KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_Y, 0.0f) + .toFloat(); + + // Load visible markers settings + visibilityFilter.generic.set(conf.value(KEY_VISIBLE_MARKER_GENERIC, true).toBool()); + visibilityFilter.herb.set(conf.value(KEY_VISIBLE_MARKER_HERB, true).toBool()); + visibilityFilter.river.set(conf.value(KEY_VISIBLE_MARKER_RIVER, true).toBool()); + visibilityFilter.place.set(conf.value(KEY_VISIBLE_MARKER_PLACE, true).toBool()); + visibilityFilter.mob.set(conf.value(KEY_VISIBLE_MARKER_MOB, true).toBool()); + visibilityFilter.comment.set(conf.value(KEY_VISIBLE_MARKER_COMMENT, true).toBool()); + visibilityFilter.road.set(conf.value(KEY_VISIBLE_MARKER_ROAD, true).toBool()); + visibilityFilter.object.set(conf.value(KEY_VISIBLE_MARKER_OBJECT, true).toBool()); + visibilityFilter.action.set(conf.value(KEY_VISIBLE_MARKER_ACTION, true).toBool()); + visibilityFilter.locality.set(conf.value(KEY_VISIBLE_MARKER_LOCALITY, true).toBool()); + visibilityFilter.connections.set(conf.value(KEY_VISIBLE_CONNECTIONS, true).toBool()); } void Configuration::AccountSettings::read(const QSettings &conf) @@ -691,11 +814,15 @@ void Configuration::GroupManagerSettings::read(const QSettings &conf) npcSortBottom = conf.value(KEY_GROUP_NPC_SORT_BOTTOM, false).toBool(); } +Configuration::MumeClockSettings::MumeClockSettings() = default; + void Configuration::MumeClockSettings::read(const QSettings &conf) { // NOTE: old values might be stored as int32 startEpoch = conf.value(KEY_MUME_START_EPOCH, 1517443173).toLongLong(); display = conf.value(KEY_DISPLAY_CLOCK, true).toBool(); + gmcpBroadcast.set(conf.value(KEY_GMCP_BROADCAST_CLOCK, true).toBool()); + gmcpBroadcastInterval.set(conf.value(KEY_GMCP_BROADCAST_INTERVAL, 2500).toInt()); } void Configuration::AdventurePanelSettings::read(const QSettings &conf) @@ -770,11 +897,14 @@ NODISCARD static auto getQColorName(const XNamedColor &color) void Configuration::CanvasSettings::write(QSettings &conf) const { conf.setValue(KEY_RESOURCES_DIRECTORY, resourcesDirectory); + conf.setValue(KEY_TEXTURE_SET, textureSetToInt(textureSet)); + conf.setValue(KEY_ENABLE_SEASONAL_TEXTURES, enableSeasonalTextures); conf.setValue(KEY_SHOW_MISSING_MAP_ID, showMissingMapId.get()); conf.setValue(KEY_SHOW_UNSAVED_CHANGES, showUnsavedChanges.get()); conf.setValue(KEY_DRAW_NOT_MAPPED_EXITS, showUnmappedExits.get()); conf.setValue(KEY_DRAW_UPPER_LAYERS_TEXTURED, drawUpperLayersTextured); conf.setValue(KEY_DRAW_DOOR_NAMES, drawDoorNames); + conf.setValue(KEY_LAYER_TRANSPARENCY, layerTransparency); conf.setValue(KEY_BACKGROUND_COLOR, getQColorName(backgroundColor)); conf.setValue(KEY_ROOM_DARK_COLOR, getQColorName(roomDarkColor)); conf.setValue(KEY_ROOM_DARK_LIT_COLOR, getQColorName(roomDarkLitColor)); @@ -788,6 +918,239 @@ void Configuration::CanvasSettings::write(QSettings &conf) const conf.setValue(KEY_3D_VERTICAL_ANGLE, advanced.verticalAngle.get()); conf.setValue(KEY_3D_HORIZONTAL_ANGLE, advanced.horizontalAngle.get()); conf.setValue(KEY_3D_LAYER_HEIGHT, advanced.layerHeight.get()); + + // Save background image settings + conf.setValue(KEY_BACKGROUND_IMAGE_ENABLED, advanced.useBackgroundImage); + conf.setValue(KEY_BACKGROUND_IMAGE_PATH, advanced.backgroundImagePath); + conf.setValue(KEY_BACKGROUND_IMAGE_FIT_MODE, advanced.backgroundFitMode); + conf.setValue(KEY_BACKGROUND_IMAGE_OPACITY, advanced.backgroundOpacity); + conf.setValue(KEY_BACKGROUND_IMAGE_FOCUSED_SCALE, advanced.backgroundFocusedScale); + conf.setValue(KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_X, advanced.backgroundFocusedOffsetX); + conf.setValue(KEY_BACKGROUND_IMAGE_FOCUSED_OFFSET_Y, advanced.backgroundFocusedOffsetY); + + // Save visible markers settings + conf.setValue(KEY_VISIBLE_MARKER_GENERIC, visibilityFilter.generic.get()); + conf.setValue(KEY_VISIBLE_MARKER_HERB, visibilityFilter.herb.get()); + conf.setValue(KEY_VISIBLE_MARKER_RIVER, visibilityFilter.river.get()); + conf.setValue(KEY_VISIBLE_MARKER_PLACE, visibilityFilter.place.get()); + conf.setValue(KEY_VISIBLE_MARKER_MOB, visibilityFilter.mob.get()); + conf.setValue(KEY_VISIBLE_MARKER_COMMENT, visibilityFilter.comment.get()); + conf.setValue(KEY_VISIBLE_MARKER_ROAD, visibilityFilter.road.get()); + conf.setValue(KEY_VISIBLE_MARKER_OBJECT, visibilityFilter.object.get()); + conf.setValue(KEY_VISIBLE_MARKER_ACTION, visibilityFilter.action.get()); + conf.setValue(KEY_VISIBLE_MARKER_LOCALITY, visibilityFilter.locality.get()); + conf.setValue(KEY_VISIBLE_CONNECTIONS, visibilityFilter.connections.get()); +} + +void Configuration::Hotkeys::read(const QSettings &conf) +{ + // File operations + fileOpen.set(conf.value(KEY_HOTKEY_FILE_OPEN, "Ctrl+O").toString()); + fileSave.set(conf.value(KEY_HOTKEY_FILE_SAVE, "Ctrl+S").toString()); + fileReload.set(conf.value(KEY_HOTKEY_FILE_RELOAD, "Ctrl+R").toString()); + fileQuit.set(conf.value(KEY_HOTKEY_FILE_QUIT, "Ctrl+Q").toString()); + + // Edit operations + editUndo.set(conf.value(KEY_HOTKEY_EDIT_UNDO, "Ctrl+Z").toString()); + editRedo.set(conf.value(KEY_HOTKEY_EDIT_REDO, "Ctrl+Y").toString()); + editPreferences.set(conf.value(KEY_HOTKEY_EDIT_PREFERENCES, "Ctrl+P").toString()); + editPreferencesAlt.set(conf.value(KEY_HOTKEY_EDIT_PREFERENCES_ALT, "Esc").toString()); + editFindRooms.set(conf.value(KEY_HOTKEY_EDIT_FIND_ROOMS, "Ctrl+F").toString()); + editRoom.set(conf.value(KEY_HOTKEY_EDIT_ROOM, "Ctrl+E").toString()); + + // View operations + viewZoomIn.set(conf.value(KEY_HOTKEY_VIEW_ZOOM_IN, "").toString()); + viewZoomOut.set(conf.value(KEY_HOTKEY_VIEW_ZOOM_OUT, "").toString()); + viewZoomReset.set(conf.value(KEY_HOTKEY_VIEW_ZOOM_RESET, "Ctrl+0").toString()); + viewLayerUp.set(conf.value(KEY_HOTKEY_VIEW_LAYER_UP, "").toString()); + viewLayerDown.set(conf.value(KEY_HOTKEY_VIEW_LAYER_DOWN, "").toString()); + viewLayerReset.set(conf.value(KEY_HOTKEY_VIEW_LAYER_RESET, "").toString()); + + // View toggles + viewRadialTransparency.set(conf.value(KEY_HOTKEY_VIEW_RADIAL_TRANSPARENCY, "").toString()); + viewStatusBar.set(conf.value(KEY_HOTKEY_VIEW_STATUS_BAR, "").toString()); + viewScrollBars.set(conf.value(KEY_HOTKEY_VIEW_SCROLL_BARS, "").toString()); + viewMenuBar.set(conf.value(KEY_HOTKEY_VIEW_MENU_BAR, "").toString()); + viewAlwaysOnTop.set(conf.value(KEY_HOTKEY_VIEW_ALWAYS_ON_TOP, "").toString()); + + // Side panels + panelLog.set(conf.value(KEY_HOTKEY_PANEL_LOG, "Ctrl+L").toString()); + panelClient.set(conf.value(KEY_HOTKEY_PANEL_CLIENT, "").toString()); + panelGroup.set(conf.value(KEY_HOTKEY_PANEL_GROUP, "").toString()); + panelRoom.set(conf.value(KEY_HOTKEY_PANEL_ROOM, "").toString()); + panelAdventure.set(conf.value(KEY_HOTKEY_PANEL_ADVENTURE, "").toString()); + panelComms.set(conf.value(KEY_HOTKEY_PANEL_COMMS, "").toString()); + panelDescription.set(conf.value(KEY_HOTKEY_PANEL_DESCRIPTION, "").toString()); + + // Mouse modes + modeMoveMap.set(conf.value(KEY_HOTKEY_MODE_MOVE_MAP, "").toString()); + modeRaypick.set(conf.value(KEY_HOTKEY_MODE_RAYPICK, "").toString()); + modeSelectRooms.set(conf.value(KEY_HOTKEY_MODE_SELECT_ROOMS, "").toString()); + modeSelectMarkers.set(conf.value(KEY_HOTKEY_MODE_SELECT_MARKERS, "").toString()); + modeSelectConnection.set(conf.value(KEY_HOTKEY_MODE_SELECT_CONNECTION, "").toString()); + modeCreateMarker.set(conf.value(KEY_HOTKEY_MODE_CREATE_MARKER, "").toString()); + modeCreateRoom.set(conf.value(KEY_HOTKEY_MODE_CREATE_ROOM, "").toString()); + modeCreateConnection.set(conf.value(KEY_HOTKEY_MODE_CREATE_CONNECTION, "").toString()); + modeCreateOnewayConnection.set( + conf.value(KEY_HOTKEY_MODE_CREATE_ONEWAY_CONNECTION, "").toString()); + + // Room operations + roomCreate.set(conf.value(KEY_HOTKEY_ROOM_CREATE, "").toString()); + roomMoveUp.set(conf.value(KEY_HOTKEY_ROOM_MOVE_UP, "").toString()); + roomMoveDown.set(conf.value(KEY_HOTKEY_ROOM_MOVE_DOWN, "").toString()); + roomMergeUp.set(conf.value(KEY_HOTKEY_ROOM_MERGE_UP, "").toString()); + roomMergeDown.set(conf.value(KEY_HOTKEY_ROOM_MERGE_DOWN, "").toString()); + roomDelete.set(conf.value(KEY_HOTKEY_ROOM_DELETE, "Del").toString()); + roomConnectNeighbors.set(conf.value(KEY_HOTKEY_ROOM_CONNECT_NEIGHBORS, "").toString()); + roomMoveToSelected.set(conf.value(KEY_HOTKEY_ROOM_MOVE_TO_SELECTED, "").toString()); + roomUpdateSelected.set(conf.value(KEY_HOTKEY_ROOM_UPDATE_SELECTED, "").toString()); +} + +void Configuration::Hotkeys::write(QSettings &conf) const +{ + // File operations + conf.setValue(KEY_HOTKEY_FILE_OPEN, fileOpen.get()); + conf.setValue(KEY_HOTKEY_FILE_SAVE, fileSave.get()); + conf.setValue(KEY_HOTKEY_FILE_RELOAD, fileReload.get()); + conf.setValue(KEY_HOTKEY_FILE_QUIT, fileQuit.get()); + + // Edit operations + conf.setValue(KEY_HOTKEY_EDIT_UNDO, editUndo.get()); + conf.setValue(KEY_HOTKEY_EDIT_REDO, editRedo.get()); + conf.setValue(KEY_HOTKEY_EDIT_PREFERENCES, editPreferences.get()); + conf.setValue(KEY_HOTKEY_EDIT_PREFERENCES_ALT, editPreferencesAlt.get()); + conf.setValue(KEY_HOTKEY_EDIT_FIND_ROOMS, editFindRooms.get()); + conf.setValue(KEY_HOTKEY_EDIT_ROOM, editRoom.get()); + + // View operations + conf.setValue(KEY_HOTKEY_VIEW_ZOOM_IN, viewZoomIn.get()); + conf.setValue(KEY_HOTKEY_VIEW_ZOOM_OUT, viewZoomOut.get()); + conf.setValue(KEY_HOTKEY_VIEW_ZOOM_RESET, viewZoomReset.get()); + conf.setValue(KEY_HOTKEY_VIEW_LAYER_UP, viewLayerUp.get()); + conf.setValue(KEY_HOTKEY_VIEW_LAYER_DOWN, viewLayerDown.get()); + conf.setValue(KEY_HOTKEY_VIEW_LAYER_RESET, viewLayerReset.get()); + + // View toggles + conf.setValue(KEY_HOTKEY_VIEW_RADIAL_TRANSPARENCY, viewRadialTransparency.get()); + conf.setValue(KEY_HOTKEY_VIEW_STATUS_BAR, viewStatusBar.get()); + conf.setValue(KEY_HOTKEY_VIEW_SCROLL_BARS, viewScrollBars.get()); + conf.setValue(KEY_HOTKEY_VIEW_MENU_BAR, viewMenuBar.get()); + conf.setValue(KEY_HOTKEY_VIEW_ALWAYS_ON_TOP, viewAlwaysOnTop.get()); + + // Side panels + conf.setValue(KEY_HOTKEY_PANEL_LOG, panelLog.get()); + conf.setValue(KEY_HOTKEY_PANEL_CLIENT, panelClient.get()); + conf.setValue(KEY_HOTKEY_PANEL_GROUP, panelGroup.get()); + conf.setValue(KEY_HOTKEY_PANEL_ROOM, panelRoom.get()); + conf.setValue(KEY_HOTKEY_PANEL_ADVENTURE, panelAdventure.get()); + conf.setValue(KEY_HOTKEY_PANEL_COMMS, panelComms.get()); + conf.setValue(KEY_HOTKEY_PANEL_DESCRIPTION, panelDescription.get()); + + // Mouse modes + conf.setValue(KEY_HOTKEY_MODE_MOVE_MAP, modeMoveMap.get()); + conf.setValue(KEY_HOTKEY_MODE_RAYPICK, modeRaypick.get()); + conf.setValue(KEY_HOTKEY_MODE_SELECT_ROOMS, modeSelectRooms.get()); + conf.setValue(KEY_HOTKEY_MODE_SELECT_MARKERS, modeSelectMarkers.get()); + conf.setValue(KEY_HOTKEY_MODE_SELECT_CONNECTION, modeSelectConnection.get()); + conf.setValue(KEY_HOTKEY_MODE_CREATE_MARKER, modeCreateMarker.get()); + conf.setValue(KEY_HOTKEY_MODE_CREATE_ROOM, modeCreateRoom.get()); + conf.setValue(KEY_HOTKEY_MODE_CREATE_CONNECTION, modeCreateConnection.get()); + conf.setValue(KEY_HOTKEY_MODE_CREATE_ONEWAY_CONNECTION, modeCreateOnewayConnection.get()); + + // Room operations + conf.setValue(KEY_HOTKEY_ROOM_CREATE, roomCreate.get()); + conf.setValue(KEY_HOTKEY_ROOM_MOVE_UP, roomMoveUp.get()); + conf.setValue(KEY_HOTKEY_ROOM_MOVE_DOWN, roomMoveDown.get()); + conf.setValue(KEY_HOTKEY_ROOM_MERGE_UP, roomMergeUp.get()); + conf.setValue(KEY_HOTKEY_ROOM_MERGE_DOWN, roomMergeDown.get()); + conf.setValue(KEY_HOTKEY_ROOM_DELETE, roomDelete.get()); + conf.setValue(KEY_HOTKEY_ROOM_CONNECT_NEIGHBORS, roomConnectNeighbors.get()); + conf.setValue(KEY_HOTKEY_ROOM_MOVE_TO_SELECTED, roomMoveToSelected.get()); + conf.setValue(KEY_HOTKEY_ROOM_UPDATE_SELECTED, roomUpdateSelected.get()); +} + +void Configuration::CommsSettings::read(const QSettings &conf) +{ + // Communication colors + tellColor.set(conf.value(tellColor.getName(), QColor(32, 108, 9)).value()); + whisperColor.set(conf.value(whisperColor.getName(), QColor(103, 135, 149)).value()); + groupColor.set(conf.value(groupColor.getName(), QColor(15, 123, 255)).value()); + askColor.set(conf.value(askColor.getName(), QColor(Qt::yellow)).value()); + sayColor.set(conf.value(sayColor.getName(), QColor(80, 173, 199)).value()); + emoteColor.set(conf.value(emoteColor.getName(), QColor(203, 37, 111)).value()); + socialColor.set(conf.value(socialColor.getName(), QColor(217, 140, 151)).value()); + yellColor.set(conf.value(yellColor.getName(), QColor(176, 80, 189)).value()); + narrateColor.set(conf.value(narrateColor.getName(), QColor(119, 197, 203)).value()); + prayColor.set(conf.value(prayColor.getName(), QColor(173, 216, 230)).value()); + shoutColor.set(conf.value(shoutColor.getName(), QColor(160, 9, 198)).value()); + singColor.set(conf.value(singColor.getName(), QColor(144, 238, 144)).value()); + backgroundColor.set(conf.value(backgroundColor.getName(), QColor(22, 31, 33)).value()); + + // Font styling options + yellAllCaps.set(conf.value(yellAllCaps.getName(), true).toBool()); + whisperItalic.set(conf.value(whisperItalic.getName(), true).toBool()); + emoteItalic.set(conf.value(emoteItalic.getName(), true).toBool()); + + // Display options + showTimestamps.set(conf.value(showTimestamps.getName(), false).toBool()); + saveLogOnExit.set(conf.value(saveLogOnExit.getName(), false).toBool()); + logDirectory.set(conf.value(logDirectory.getName(), QString("")).toString()); + + // Talker colors + talkerYouColor.set(conf.value(talkerYouColor.getName(), QColor(228, 250, 255)).value()); + talkerPlayerColor.set( + conf.value(talkerPlayerColor.getName(), QColor(255, 187, 16)).value()); + talkerNpcColor.set(conf.value(talkerNpcColor.getName(), QColor(25, 138, 23)).value()); + talkerAllyColor.set(conf.value(talkerAllyColor.getName(), QColor(33, 166, 255)).value()); + talkerNeutralColor.set( + conf.value(talkerNeutralColor.getName(), QColor(166, 168, 168)).value()); + talkerEnemyColor.set(conf.value(talkerEnemyColor.getName(), QColor(173, 7, 37)).value()); + + // Tab muting (filters) + muteDirectTab.set(conf.value(muteDirectTab.getName(), false).toBool()); + muteLocalTab.set(conf.value(muteLocalTab.getName(), false).toBool()); + muteGlobalTab.set(conf.value(muteGlobalTab.getName(), false).toBool()); +} + +void Configuration::CommsSettings::write(QSettings &conf) const +{ + // Communication colors + conf.setValue(tellColor.getName(), tellColor.get()); + conf.setValue(whisperColor.getName(), whisperColor.get()); + conf.setValue(groupColor.getName(), groupColor.get()); + conf.setValue(askColor.getName(), askColor.get()); + conf.setValue(sayColor.getName(), sayColor.get()); + conf.setValue(emoteColor.getName(), emoteColor.get()); + conf.setValue(socialColor.getName(), socialColor.get()); + conf.setValue(yellColor.getName(), yellColor.get()); + conf.setValue(narrateColor.getName(), narrateColor.get()); + conf.setValue(prayColor.getName(), prayColor.get()); + conf.setValue(shoutColor.getName(), shoutColor.get()); + conf.setValue(singColor.getName(), singColor.get()); + conf.setValue(backgroundColor.getName(), backgroundColor.get()); + + // Font styling options + conf.setValue(yellAllCaps.getName(), yellAllCaps.get()); + conf.setValue(whisperItalic.getName(), whisperItalic.get()); + conf.setValue(emoteItalic.getName(), emoteItalic.get()); + + // Display options + conf.setValue(showTimestamps.getName(), showTimestamps.get()); + conf.setValue(saveLogOnExit.getName(), saveLogOnExit.get()); + conf.setValue(logDirectory.getName(), logDirectory.get()); + + // Talker colors + conf.setValue(talkerYouColor.getName(), talkerYouColor.get()); + conf.setValue(talkerPlayerColor.getName(), talkerPlayerColor.get()); + conf.setValue(talkerNpcColor.getName(), talkerNpcColor.get()); + conf.setValue(talkerAllyColor.getName(), talkerAllyColor.get()); + conf.setValue(talkerNeutralColor.getName(), talkerNeutralColor.get()); + conf.setValue(talkerEnemyColor.getName(), talkerEnemyColor.get()); + + // Tab muting (filters) + conf.setValue(muteDirectTab.getName(), muteDirectTab.get()); + conf.setValue(muteLocalTab.getName(), muteLocalTab.get()); + conf.setValue(muteGlobalTab.getName(), muteGlobalTab.get()); } void Configuration::AccountSettings::write(QSettings &conf) const @@ -862,6 +1225,8 @@ void Configuration::MumeClockSettings::write(QSettings &conf) const // Note: There's no QVariant(int64_t) constructor. conf.setValue(KEY_MUME_START_EPOCH, static_cast(startEpoch)); conf.setValue(KEY_DISPLAY_CLOCK, display); + conf.setValue(KEY_GMCP_BROADCAST_CLOCK, gmcpBroadcast.get()); + conf.setValue(KEY_GMCP_BROADCAST_INTERVAL, gmcpBroadcastInterval.get()); } void Configuration::AdventurePanelSettings::write(QSettings &conf) const @@ -994,6 +1359,118 @@ void Configuration::CanvasSettings::Advanced::registerChangeCallback( layerHeight.registerChangeCallback(lifetime, callback); } +Configuration::CanvasSettings::VisibilityFilter::VisibilityFilter() = default; + +bool Configuration::CanvasSettings::VisibilityFilter::isVisible(InfomarkClassEnum markerClass) const +{ + switch (markerClass) { + case InfomarkClassEnum::GENERIC: + return generic.get(); + case InfomarkClassEnum::HERB: + return herb.get(); + case InfomarkClassEnum::RIVER: + return river.get(); + case InfomarkClassEnum::PLACE: + return place.get(); + case InfomarkClassEnum::MOB: + return mob.get(); + case InfomarkClassEnum::COMMENT: + return comment.get(); + case InfomarkClassEnum::ROAD: + return road.get(); + case InfomarkClassEnum::OBJECT: + return object.get(); + case InfomarkClassEnum::ACTION: + return action.get(); + case InfomarkClassEnum::LOCALITY: + return locality.get(); + } + return true; // Default to visible for unknown types +} + +void Configuration::CanvasSettings::VisibilityFilter::setVisible(InfomarkClassEnum markerClass, + bool visible) +{ + switch (markerClass) { + case InfomarkClassEnum::GENERIC: + generic.set(visible); + break; + case InfomarkClassEnum::HERB: + herb.set(visible); + break; + case InfomarkClassEnum::RIVER: + river.set(visible); + break; + case InfomarkClassEnum::PLACE: + place.set(visible); + break; + case InfomarkClassEnum::MOB: + mob.set(visible); + break; + case InfomarkClassEnum::COMMENT: + comment.set(visible); + break; + case InfomarkClassEnum::ROAD: + road.set(visible); + break; + case InfomarkClassEnum::OBJECT: + object.set(visible); + break; + case InfomarkClassEnum::ACTION: + action.set(visible); + break; + case InfomarkClassEnum::LOCALITY: + locality.set(visible); + break; + } +} + +void Configuration::CanvasSettings::VisibilityFilter::showAll() +{ + generic.set(true); + herb.set(true); + river.set(true); + place.set(true); + mob.set(true); + comment.set(true); + road.set(true); + object.set(true); + action.set(true); + locality.set(true); + connections.set(true); +} + +void Configuration::CanvasSettings::VisibilityFilter::hideAll() +{ + generic.set(false); + herb.set(false); + river.set(false); + place.set(false); + mob.set(false); + comment.set(false); + road.set(false); + object.set(false); + action.set(false); + locality.set(false); + connections.set(false); +} + +void Configuration::CanvasSettings::VisibilityFilter::registerChangeCallback( + const ChangeMonitor::Lifetime &lifetime, const ChangeMonitor::Function &callback) +{ + generic.registerChangeCallback(lifetime, callback); + herb.registerChangeCallback(lifetime, callback); + river.registerChangeCallback(lifetime, callback); + place.registerChangeCallback(lifetime, callback); + mob.registerChangeCallback(lifetime, callback); + comment.registerChangeCallback(lifetime, callback); + road.registerChangeCallback(lifetime, callback); + object.registerChangeCallback(lifetime, callback); + action.registerChangeCallback(lifetime, callback); + locality.registerChangeCallback(lifetime, callback); + connections.registerChangeCallback(lifetime, callback); +} + void setEnteredMain() { g_thread = std::this_thread::get_id(); diff --git a/src/configuration/configuration.h b/src/configuration/configuration.h index 58d1d1e9f..d83f60ce5 100644 --- a/src/configuration/configuration.h +++ b/src/configuration/configuration.h @@ -29,6 +29,9 @@ #undef TRANSPARENT // Bad dog, Microsoft; bad dog!!! +// Forward declaration for InfomarkClassEnum +enum class InfomarkClassEnum : uint8_t; + #define SUBGROUP() \ friend class Configuration; \ void read(const QSettings &conf); \ @@ -79,6 +82,7 @@ class NODISCARD Configuration final char prefixChar = char_consts::C_UNDERSCORE; bool encodeEmoji = true; bool decodeEmoji = true; + bool enableYellFallbackParsing = true; // Parse yells from game text when GMCP unavailable private: SUBGROUP(); @@ -143,6 +147,10 @@ class NODISCARD Configuration final bool trilinearFiltering = false; bool softwareOpenGL = false; QString resourcesDirectory; + TextureSetEnum textureSet = TextureSetEnum::MODERN; + bool enableSeasonalTextures = true; + float layerTransparency = 1.0f; // 0.0 = only focused layer, 1.0 = maximum transparency + bool enableRadialTransparency = true; // Enable radial transparency zones on upper layers // not saved yet: bool drawCharBeacons = true; @@ -159,12 +167,21 @@ class NODISCARD Configuration final NamedConfig autoTilt{"MMAPPER_AUTO_TILT", true}; NamedConfig printPerfStats{"MMAPPER_GL_PERFSTATS", IS_DEBUG_BUILD}; + // Background image settings + bool useBackgroundImage = false; + QString backgroundImagePath; + int backgroundFitMode = 0; // BackgroundFitModeEnum::FIT + float backgroundOpacity = 1.0f; + float backgroundFocusedScale = 1.0f; // Scale factor for FOCUSED mode (0.1 to 10.0) + float backgroundFocusedOffsetX = 0.0f; // X offset for FOCUSED mode (-1000 to 1000) + float backgroundFocusedOffsetY = 0.0f; // Y offset for FOCUSED mode (-1000 to 1000) + // 5..90 degrees FixedPoint<1> fov{50, 900, 765}; // 0..90 degrees FixedPoint<1> verticalAngle{0, 900, 450}; - // -45..45 degrees - FixedPoint<1> horizontalAngle{-450, 450, 0}; + // -180..180 degrees (full rotation) + FixedPoint<1> horizontalAngle{-1800, 1800, 0}; // 1..10 rooms FixedPoint<1> layerHeight{10, 100, 15}; @@ -175,10 +192,147 @@ class NODISCARD Configuration final Advanced(); } advanced; + struct NODISCARD VisibilityFilter final + { + NamedConfig generic{"VISIBLE_MARKER_GENERIC", true}; + NamedConfig herb{"VISIBLE_MARKER_HERB", true}; + NamedConfig river{"VISIBLE_MARKER_RIVER", true}; + NamedConfig place{"VISIBLE_MARKER_PLACE", true}; + NamedConfig mob{"VISIBLE_MARKER_MOB", true}; + NamedConfig comment{"VISIBLE_MARKER_COMMENT", true}; + NamedConfig road{"VISIBLE_MARKER_ROAD", true}; + NamedConfig object{"VISIBLE_MARKER_OBJECT", true}; + NamedConfig action{"VISIBLE_MARKER_ACTION", true}; + NamedConfig locality{"VISIBLE_MARKER_LOCALITY", true}; + NamedConfig connections{"VISIBLE_CONNECTIONS", true}; + + public: + NODISCARD bool isVisible(InfomarkClassEnum markerClass) const; + void setVisible(InfomarkClassEnum markerClass, bool visible); + NODISCARD bool isConnectionsVisible() const { return connections.get(); } + void setConnectionsVisible(bool visible) { connections.set(visible); } + void showAll(); + void hideAll(); + void registerChangeCallback(const ChangeMonitor::Lifetime &lifetime, + const ChangeMonitor::Function &callback); + + VisibilityFilter(); + } visibilityFilter; + private: SUBGROUP(); } canvas; + struct NODISCARD Hotkeys final + { + // File operations + NamedConfig fileOpen{"HOTKEY_FILE_OPEN", "Ctrl+O"}; + NamedConfig fileSave{"HOTKEY_FILE_SAVE", "Ctrl+S"}; + NamedConfig fileReload{"HOTKEY_FILE_RELOAD", "Ctrl+R"}; + NamedConfig fileQuit{"HOTKEY_FILE_QUIT", "Ctrl+Q"}; + + // Edit operations + NamedConfig editUndo{"HOTKEY_EDIT_UNDO", "Ctrl+Z"}; + NamedConfig editRedo{"HOTKEY_EDIT_REDO", "Ctrl+Y"}; + NamedConfig editPreferences{"HOTKEY_EDIT_PREFERENCES", "Ctrl+P"}; + NamedConfig editPreferencesAlt{"HOTKEY_EDIT_PREFERENCES_ALT", "Esc"}; + NamedConfig editFindRooms{"HOTKEY_EDIT_FIND_ROOMS", "Ctrl+F"}; + NamedConfig editRoom{"HOTKEY_EDIT_ROOM", "Ctrl+E"}; + + // View operations + NamedConfig viewZoomIn{"HOTKEY_VIEW_ZOOM_IN", ""}; + NamedConfig viewZoomOut{"HOTKEY_VIEW_ZOOM_OUT", ""}; + NamedConfig viewZoomReset{"HOTKEY_VIEW_ZOOM_RESET", "Ctrl+0"}; + NamedConfig viewLayerUp{"HOTKEY_VIEW_LAYER_UP", ""}; + NamedConfig viewLayerDown{"HOTKEY_VIEW_LAYER_DOWN", ""}; + NamedConfig viewLayerReset{"HOTKEY_VIEW_LAYER_RESET", ""}; + + // View toggles + NamedConfig viewRadialTransparency{"HOTKEY_VIEW_RADIAL_TRANSPARENCY", ""}; + NamedConfig viewStatusBar{"HOTKEY_VIEW_STATUS_BAR", ""}; + NamedConfig viewScrollBars{"HOTKEY_VIEW_SCROLL_BARS", ""}; + NamedConfig viewMenuBar{"HOTKEY_VIEW_MENU_BAR", ""}; + NamedConfig viewAlwaysOnTop{"HOTKEY_VIEW_ALWAYS_ON_TOP", ""}; + + // Side panels + NamedConfig panelLog{"HOTKEY_PANEL_LOG", "Ctrl+L"}; + NamedConfig panelClient{"HOTKEY_PANEL_CLIENT", ""}; + NamedConfig panelGroup{"HOTKEY_PANEL_GROUP", ""}; + NamedConfig panelRoom{"HOTKEY_PANEL_ROOM", ""}; + NamedConfig panelAdventure{"HOTKEY_PANEL_ADVENTURE", ""}; + NamedConfig panelDescription{"HOTKEY_PANEL_DESCRIPTION", ""}; + NamedConfig panelComms{"HOTKEY_PANEL_COMMS", ""}; + + // Mouse modes + NamedConfig modeMoveMap{"HOTKEY_MODE_MOVE_MAP", ""}; + NamedConfig modeRaypick{"HOTKEY_MODE_RAYPICK", ""}; + NamedConfig modeSelectRooms{"HOTKEY_MODE_SELECT_ROOMS", ""}; + NamedConfig modeSelectMarkers{"HOTKEY_MODE_SELECT_MARKERS", ""}; + NamedConfig modeSelectConnection{"HOTKEY_MODE_SELECT_CONNECTION", ""}; + NamedConfig modeCreateMarker{"HOTKEY_MODE_CREATE_MARKER", ""}; + NamedConfig modeCreateRoom{"HOTKEY_MODE_CREATE_ROOM", ""}; + NamedConfig modeCreateConnection{"HOTKEY_MODE_CREATE_CONNECTION", ""}; + NamedConfig modeCreateOnewayConnection{"HOTKEY_MODE_CREATE_ONEWAY_CONNECTION", ""}; + + // Room operations + NamedConfig roomCreate{"HOTKEY_ROOM_CREATE", ""}; + NamedConfig roomMoveUp{"HOTKEY_ROOM_MOVE_UP", ""}; + NamedConfig roomMoveDown{"HOTKEY_ROOM_MOVE_DOWN", ""}; + NamedConfig roomMergeUp{"HOTKEY_ROOM_MERGE_UP", ""}; + NamedConfig roomMergeDown{"HOTKEY_ROOM_MERGE_DOWN", ""}; + NamedConfig roomDelete{"HOTKEY_ROOM_DELETE", "Del"}; + NamedConfig roomConnectNeighbors{"HOTKEY_ROOM_CONNECT_NEIGHBORS", ""}; + NamedConfig roomMoveToSelected{"HOTKEY_ROOM_MOVE_TO_SELECTED", ""}; + NamedConfig roomUpdateSelected{"HOTKEY_ROOM_UPDATE_SELECTED", ""}; + + private: + SUBGROUP(); + } hotkeys; + + struct NODISCARD CommsSettings final + { + // Colors for each communication type + NamedConfig tellColor{"COMMS_TELL_COLOR", QColor(Qt::cyan)}; + NamedConfig whisperColor{"COMMS_WHISPER_COLOR", QColor(135, 206, 250)}; // Light sky blue + NamedConfig groupColor{"COMMS_GROUP_COLOR", QColor(Qt::green)}; + NamedConfig askColor{"COMMS_ASK_COLOR", QColor(Qt::yellow)}; + NamedConfig sayColor{"COMMS_SAY_COLOR", QColor(Qt::white)}; + NamedConfig emoteColor{"COMMS_EMOTE_COLOR", QColor(Qt::magenta)}; + NamedConfig socialColor{"COMMS_SOCIAL_COLOR", QColor(255, 182, 193)}; // Light pink + NamedConfig yellColor{"COMMS_YELL_COLOR", QColor(Qt::red)}; + NamedConfig narrateColor{"COMMS_NARRATE_COLOR", QColor(255, 165, 0)}; // Orange + NamedConfig prayColor{"COMMS_PRAY_COLOR", QColor(173, 216, 230)}; // Light blue + NamedConfig shoutColor{"COMMS_SHOUT_COLOR", QColor(139, 0, 0)}; // Dark red + NamedConfig singColor{"COMMS_SING_COLOR", QColor(144, 238, 144)}; // Light green + NamedConfig backgroundColor{"COMMS_BG_COLOR", QColor(Qt::black)}; + + // Talker colors (based on GMCP Comm.Channel talker-type) + NamedConfig talkerYouColor{"COMMS_TALKER_YOU_COLOR", QColor(255, 215, 0)}; // Gold + NamedConfig talkerPlayerColor{"COMMS_TALKER_PLAYER_COLOR", QColor(Qt::white)}; + NamedConfig talkerNpcColor{"COMMS_TALKER_NPC_COLOR", QColor(192, 192, 192)}; // Silver/Gray + NamedConfig talkerAllyColor{"COMMS_TALKER_ALLY_COLOR", QColor(0, 255, 0)}; // Bright green + NamedConfig talkerNeutralColor{"COMMS_TALKER_NEUTRAL_COLOR", QColor(255, 255, 0)}; // Yellow + NamedConfig talkerEnemyColor{"COMMS_TALKER_ENEMY_COLOR", QColor(255, 0, 0)}; // Red + + // Font styling options + NamedConfig yellAllCaps{"COMMS_YELL_ALL_CAPS", true}; + NamedConfig whisperItalic{"COMMS_WHISPER_ITALIC", true}; + NamedConfig emoteItalic{"COMMS_EMOTE_ITALIC", true}; + + // Display options + NamedConfig showTimestamps{"COMMS_SHOW_TIMESTAMPS", false}; + NamedConfig saveLogOnExit{"COMMS_SAVE_LOG_ON_EXIT", false}; + NamedConfig logDirectory{"COMMS_LOG_DIR", ""}; + + // Tab muting (acts as a filter) + NamedConfig muteDirectTab{"COMMS_MUTE_DIRECT", false}; + NamedConfig muteLocalTab{"COMMS_MUTE_LOCAL", false}; + NamedConfig muteGlobalTab{"COMMS_MUTE_GLOBAL", false}; + + private: + SUBGROUP(); + } comms; + #define XFOREACH_NAMED_COLOR_OPTIONS(X) \ X(BACKGROUND, BACKGROUND_NAME) \ X(CONNECTION_NORMAL, CONNECTION_NORMAL_NAME) \ @@ -300,6 +454,10 @@ class NODISCARD Configuration final { int64_t startEpoch = 0; bool display = false; + NamedConfig gmcpBroadcast{"GMCP_BROADCAST_CLOCK", true}; // Enable GMCP clock broadcasting + NamedConfig gmcpBroadcastInterval{"GMCP_BROADCAST_INTERVAL", 2500}; // Update interval in milliseconds (default: 2.5 seconds = 1 MUME minute) + + MumeClockSettings(); private: SUBGROUP(); diff --git a/src/display/MapCanvasRoomDrawer.cpp b/src/display/MapCanvasRoomDrawer.cpp index f998454de..25c617b7a 100644 --- a/src/display/MapCanvasRoomDrawer.cpp +++ b/src/display/MapCanvasRoomDrawer.cpp @@ -4,6 +4,7 @@ #include "MapCanvasRoomDrawer.h" +#include "../clock/mumemoment.h" #include "../configuration/NamedConfig.h" #include "../configuration/configuration.h" #include "../global/Array.h" @@ -227,14 +228,21 @@ IRoomVisitorCallbacks::~IRoomVisitorCallbacks() = default; static void visitRoom(const RoomHandle &room, const mctp::MapCanvasTexturesProxy &textures, IRoomVisitorCallbacks &callbacks, - const VisitRoomOptions &visitRoomOptions) + const VisitRoomOptions &visitRoomOptions, + const MumeTimeEnum timeOfDay, + const std::optional playerRoom, + const bool playerHasLight) { if (!callbacks.acceptRoom(room)) { return; } + // Check if this is the player's current room + const bool isPlayerRoom = playerRoom.has_value() && room.getId() == playerRoom.value(); + // const auto &pos = room.getPosition(); - const bool isDark = room.getLightType() == RoomLightEnum::DARK; + const RoomLightEnum roomLight = room.getLightType(); + const RoomTerrainEnum terrain = room.getTerrainType(); const bool hasNoSundeath = room.getSundeathType() == RoomSundeathEnum::NO_SUNDEATH; const bool notRideable = room.getRidableType() == RoomRidableEnum::NOT_RIDABLE; const auto terrainAndTrail = getRoomTerrainAndTrail(textures, room.getRaw()); @@ -247,6 +255,38 @@ static void visitRoom(const RoomHandle &room, callbacks.visitOverlayTexture(room, trail); } + // Dynamic lighting based on time of day, room properties, terrain, and player's light + // DEFAULT: All rooms are LIT unless explicitly dark or affected by conditions below + bool isDark = false; + + // PRIORITY 0: Player's room with light source - ALWAYS LIT + if (isPlayerRoom && playerHasLight) { + isDark = false; + } + // Priority 1: Explicit DARK status (highest priority for non-player rooms - always dark) + else if (roomLight == RoomLightEnum::DARK) { + isDark = true; + } + // Priority 2: Explicit LIT status (overrides time/terrain - always lit, e.g., cities at night) + else if (roomLight == RoomLightEnum::LIT) { + isDark = false; + } + // Priority 3: UNDEFINED rooms - apply dynamic lighting rules + else { + // Rule 1: Always-dark terrains (underground areas for trolls) + if (terrain == RoomTerrainEnum::CAVERN || terrain == RoomTerrainEnum::TUNNEL) { + isDark = true; + } + // Rule 2: Time-based lighting for outdoor areas only + else if ((timeOfDay == MumeTimeEnum::NIGHT || timeOfDay == MumeTimeEnum::DUSK) + && terrain != RoomTerrainEnum::INDOORS) { + // Only outdoor (non-INDOORS) UNDEFINED rooms become dark at night/dusk + isDark = true; + } + // Rule 3: Default for all other UNDEFINED rooms: LIT + // (day time, indoors, or any terrain during day) + } + if (isDark) { callbacks.visitNamedColorTint(room, RoomTintEnum::DARK); } else if (hasNoSundeath) { @@ -395,11 +435,14 @@ static void visitRoom(const RoomHandle &room, static void visitRooms(const RoomVector &rooms, const mctp::MapCanvasTexturesProxy &textures, IRoomVisitorCallbacks &callbacks, - const VisitRoomOptions &visitRoomOptions) + const VisitRoomOptions &visitRoomOptions, + const MumeTimeEnum timeOfDay, + const std::optional playerRoom, + const bool playerHasLight) { DECL_TIMER(t, __FUNCTION__); for (const auto &room : rooms) { - visitRoom(room, textures, callbacks, visitRoomOptions); + visitRoom(room, textures, callbacks, visitRoomOptions, timeOfDay, playerRoom, playerHasLight); } } @@ -840,13 +883,16 @@ NODISCARD static LayerMeshesIntermediate generateLayerMeshes( const RoomVector &rooms, const mctp::MapCanvasTexturesProxy &textures, const OptBounds &bounds, - const VisitRoomOptions &visitRoomOptions) + const VisitRoomOptions &visitRoomOptions, + const MumeTimeEnum timeOfDay, + const std::optional playerRoom, + const bool playerHasLight) { DECL_TIMER(t, "generateLayerMeshes"); LayerBatchData data; LayerBatchBuilder builder{data, textures, bounds}; - visitRooms(rooms, textures, builder, visitRoomOptions); + visitRooms(rooms, textures, builder, visitRoomOptions, timeOfDay, playerRoom, playerHasLight); data.sort(); return data.buildIntermediate(); @@ -858,6 +904,7 @@ struct NODISCARD InternalData final : public IMapBatchesFinisher std::unordered_map batchedMeshes; BatchedConnections connectionDrawerBuffers; std::unordered_map roomNameBatches; + MumeTimeEnum timeOfDay = MumeTimeEnum::UNKNOWN; private: void virt_finish(MapBatches &output, OpenGL &gl, GLFont &font) const final; @@ -867,7 +914,10 @@ static void generateAllLayerMeshes(InternalData &internalData, const FontMetrics &font, const LayerToRooms &layerToRooms, const mctp::MapCanvasTexturesProxy &textures, - const VisitRoomOptions &visitRoomOptions) + const VisitRoomOptions &visitRoomOptions, + const MumeTimeEnum timeOfDay, + const std::optional playerRoom, + const bool playerHasLight) { // This feature has been removed, but it's passed to a lot of functions, @@ -890,7 +940,13 @@ static void generateAllLayerMeshes(InternalData &internalData, { DECL_TIMER(t3, "generateAllLayerMeshes.loop.part2"); - layerMeshes = ::generateLayerMeshes(rooms, textures, bounds, visitRoomOptions); + layerMeshes = ::generateLayerMeshes(rooms, + textures, + bounds, + visitRoomOptions, + timeOfDay, + playerRoom, + playerHasLight); } { @@ -969,7 +1025,10 @@ LayerMeshes LayerMeshesIntermediate::getLayerMeshes(OpenGL &gl) const return lm; } -void LayerMeshes::render(const int thisLayer, const int focusedLayer) +void LayerMeshes::render(const int thisLayer, + const int focusedLayer, + const glm::vec3 &playerPos, + const bool isNight) { bool disableTextures = false; if (thisLayer > focusedLayer) { @@ -981,20 +1040,66 @@ void LayerMeshes::render(const int thisLayer, const int focusedLayer) } } - const GLRenderState less = GLRenderState().withDepthFunction(DepthFunctionEnum::LESS); - const GLRenderState equal = GLRenderState().withDepthFunction(DepthFunctionEnum::EQUAL); - const GLRenderState lequal = GLRenderState().withDepthFunction(DepthFunctionEnum::LEQUAL); + GLRenderState less = GLRenderState().withDepthFunction(DepthFunctionEnum::LESS); + GLRenderState equal = GLRenderState().withDepthFunction(DepthFunctionEnum::EQUAL); + GLRenderState lequal = GLRenderState().withDepthFunction(DepthFunctionEnum::LEQUAL); + + // Set player position and layer for radial transparency + const bool enableRadialTransparency = getConfig().canvas.enableRadialTransparency; + less.uniforms.playerPos = playerPos; + less.uniforms.currentLayer = focusedLayer; + less.uniforms.enableRadialTransparency = enableRadialTransparency; + less.uniforms.texturesDisabled = disableTextures; + less.uniforms.isNight = isNight; + + equal.uniforms.playerPos = playerPos; + equal.uniforms.currentLayer = focusedLayer; + equal.uniforms.enableRadialTransparency = enableRadialTransparency; + equal.uniforms.texturesDisabled = disableTextures; + equal.uniforms.isNight = isNight; + + lequal.uniforms.playerPos = playerPos; + lequal.uniforms.currentLayer = focusedLayer; + lequal.uniforms.enableRadialTransparency = enableRadialTransparency; + lequal.uniforms.texturesDisabled = disableTextures; + lequal.uniforms.isNight = isNight; const GLRenderState less_blended = less.withBlend(BlendModeEnum::TRANSPARENCY); const GLRenderState lequal_blended = lequal.withBlend(BlendModeEnum::TRANSPARENCY); const GLRenderState equal_blended = equal.withBlend(BlendModeEnum::TRANSPARENCY); const GLRenderState equal_multiplied = equal.withBlend(BlendModeEnum::MODULATE); - const auto color = [&thisLayer, &focusedLayer]() { - if (thisLayer <= focusedLayer) { + const float layerTransparency = getConfig().canvas.layerTransparency; + const auto color = [&thisLayer, &focusedLayer, &disableTextures, layerTransparency]() { + if (thisLayer == focusedLayer) { + // Focused layer: full opacity return Colors::white.withAlpha(0.90f); + } else if (thisLayer > focusedLayer) { + // Upper layers: render up to 10 layers above with progressive fading + const int layersAbove = thisLayer - focusedLayer; + if (layersAbove > 10) { + // More than 10 layers above: don't render + return Colors::gray70.withAlpha(0.0f); + } + // User-controlled transparency: at slider=1, layer 1=1.0, layer 2=0.9, ..., layer 10=0.1 + // Base alpha decreases by 0.1 per layer: 1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1 + float baseAlpha = glm::clamp(1.1f - (layersAbove * 0.1f), 0.1f, 1.0f); + float opacity = baseAlpha * layerTransparency; + if (disableTextures) { + // Non-textured: use 70% of the opacity for better distinction + return Colors::gray70.withAlpha(opacity * 0.7f); + } else { + return Colors::gray70.withAlpha(opacity); + } + } else { + // Layers below focused: calculate decreasing opacity + const int layersBelow = focusedLayer - thisLayer; + // User-controlled transparency: at slider=1, layer 1=1.0, layer 2=0.9, ..., layer 10=0.1 + // Base alpha decreases by 0.1 per layer: 1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1 + float baseAlpha = glm::clamp(1.1f - (layersBelow * 0.1f), 0.1f, 1.0f); + float opacity = baseAlpha * layerTransparency; + return Colors::white.withAlpha(opacity); } - return Colors::gray70.withAlpha(0.20f); }(); { @@ -1003,30 +1108,37 @@ void LayerMeshes::render(const int thisLayer, const int focusedLayer) * give higher contrast for the base textures. */ if (disableTextures) { - const auto layerWhite = Colors::white.withAlpha(0.20f); - layerBoost.render(less_blended.withColor(layerWhite)); + // Use the same opacity calculation as textured layers + layerBoost.render(less_blended.withColor(color)); } else { terrain.render(less_blended.withColor(color)); } } // REVISIT: move trails to their own batch also colored by the tint? - for (const RoomTintEnum tint : ALL_ROOM_TINTS) { - static_assert(NUM_ROOM_TINTS == 2); - const auto namedColor = [tint]() -> XNamedColor { - switch (tint) { - case RoomTintEnum::DARK: - return LOOKUP_COLOR(ROOM_DARK); - case RoomTintEnum::NO_SUNDEATH: - return LOOKUP_COLOR(ROOM_NO_SUNDEATH); - } - std::abort(); - }(); + // Only render tints on the focused layer + // Tints use MODULATE blend which multiplies colors - this works well on opaque layers + // but creates washed-out appearance on transparent layers + if (thisLayer == focusedLayer) { + // Only render tints on the focused layer to avoid appearance changes with slider adjustments + for (const RoomTintEnum tint : ALL_ROOM_TINTS) { + static_assert(NUM_ROOM_TINTS == 2); + const auto namedColor = [tint]() -> XNamedColor { + switch (tint) { + case RoomTintEnum::DARK: + return LOOKUP_COLOR(ROOM_DARK); + case RoomTintEnum::NO_SUNDEATH: + return LOOKUP_COLOR(ROOM_NO_SUNDEATH); + } + std::abort(); + }(); - if (const auto optColor = getColor(namedColor)) { - tints[tint].render(equal_multiplied.withColor(optColor.value())); - } else { - assert(false); + if (const auto optColor = getColor(namedColor)) { + // Use original modulate blend at full strength for focused layer + tints[tint].render(equal_multiplied.withColor(optColor.value())); + } else { + assert(false); + } } } @@ -1051,23 +1163,17 @@ void LayerMeshes::render(const int thisLayer, const int focusedLayer) dottedWalls.render(lequal_blended.withColor(color)); } - if (thisLayer != focusedLayer) { - // Darker when below, lighter when above - const auto baseAlpha = (thisLayer < focusedLayer) ? 0.5f : 0.1f; - const auto alpha - = glm::clamp(baseAlpha + 0.03f * static_cast(std::abs(focusedLayer - thisLayer)), - 0.f, - 1.f); - const Color &baseColor = (thisLayer < focusedLayer || disableTextures) ? Colors::black - : Colors::white; - layerBoost.render(equal_blended.withColor(baseColor.withAlpha(alpha))); - } + // Don't apply any additional darkening overlay since we're using opacity-based fading + // The original code used a black overlay which made layers too dark when combined with opacity fading } void InternalData::virt_finish(MapBatches &output, OpenGL &gl, GLFont &font) const { DECL_TIMER(t, "InternalData::virt_finish"); + // Set night status for shader darkening + output.isNight = (timeOfDay == MumeTimeEnum::NIGHT || timeOfDay == MumeTimeEnum::DUSK); + { DECL_TIMER(t2, "InternalData::virt_finish batchedMeshes"); for (const auto &kv : batchedMeshes) { @@ -1095,12 +1201,16 @@ void InternalData::virt_finish(MapBatches &output, OpenGL &gl, GLFont &font) con // NOTE: All of the lamda captures are copied, including the texture data! FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTexturesProxy &textures, const std::shared_ptr &font, - const Map &map) + const Map &map, + const MumeTimeEnum timeOfDay, + const std::optional playerRoom, + const bool playerHasLight) { const auto visitRoomOptions = getVisitRoomOptions(); return std::async(std::launch::async, - [textures, font, map, visitRoomOptions]() -> SharedMapBatchFinisher { + [textures, font, map, visitRoomOptions, timeOfDay, playerRoom, playerHasLight]() + -> SharedMapBatchFinisher { ThreadLocalNamedColorRaii tlRaii{visitRoomOptions.canvasColors, visitRoomOptions.colorSettings}; DECL_TIMER(t, "[ASYNC] generateAllLayerMeshes"); @@ -1122,11 +1232,15 @@ FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTextur auto result = std::make_shared(); auto &data = deref(result); + data.timeOfDay = timeOfDay; generateAllLayerMeshes(data, deref(font), layerToRooms, textures, - visitRoomOptions); + visitRoomOptions, + timeOfDay, + playerRoom, + playerHasLight); return SharedMapBatchFinisher{result}; }); } diff --git a/src/display/MapCanvasRoomDrawer.h b/src/display/MapCanvasRoomDrawer.h index f8f2bb76f..ca62685d1 100644 --- a/src/display/MapCanvasRoomDrawer.h +++ b/src/display/MapCanvasRoomDrawer.h @@ -171,10 +171,16 @@ struct NODISCARD Batches final } }; +enum class MumeTimeEnum : uint8_t; +class RoomId; + NODISCARD FutureSharedMapBatchFinisher generateMapDataFinisher(const mctp::MapCanvasTexturesProxy &textures, const std::shared_ptr &font, - const Map &map); + const Map &map, + MumeTimeEnum timeOfDay, + std::optional playerRoom, + bool playerHasLight); extern void finish(const IMapBatchesFinisher &finisher, std::optional &batches, diff --git a/src/display/mapcanvas_gl.cpp b/src/display/mapcanvas_gl.cpp index 2d6fce4f7..74ad3951e 100644 --- a/src/display/mapcanvas_gl.cpp +++ b/src/display/mapcanvas_gl.cpp @@ -57,7 +57,8 @@ namespace MapCanvasConfig { void registerChangeCallback(const ChangeMonitor::Lifetime &lifetime, ChangeMonitor::Function callback) { - return setConfig().canvas.advanced.registerChangeCallback(lifetime, std::move(callback)); + setConfig().canvas.advanced.registerChangeCallback(lifetime, callback); + setConfig().canvas.visibilityFilter.registerChangeCallback(lifetime, std::move(callback)); } bool isIn3dMode() @@ -602,13 +603,284 @@ void MapCanvas::finishPendingMapBatches() #undef LOG } +void MapCanvas::loadBackgroundImageIfNeeded() +{ + const auto &config = getConfig().canvas.advanced; + if (!config.useBackgroundImage || config.backgroundImagePath.isEmpty()) { + m_backgroundTexture.reset(); + m_backgroundImagePath.clear(); + return; + } + + // Reload if path changed + if (m_backgroundImagePath != config.backgroundImagePath) { + try { + // Use simple MMTexture::alloc like other textures in the codebase + m_backgroundTexture = MMTexture::alloc(config.backgroundImagePath); + + // Set texture wrap mode to border with transparent color (like "Canvas" mode) + auto *tex = m_backgroundTexture->get(); + tex->setWrapMode(QOpenGLTexture::ClampToBorder); + // Set border color to transparent (0,0,0,0) + const float borderColor[4] = {0.0f, 0.0f, 0.0f, 0.0f}; + tex->setBorderColor(borderColor[0], borderColor[1], borderColor[2], borderColor[3]); + + // Allocate texture ID and register with OpenGL (like initTextures() does) + auto id = allocateTextureId(); + m_backgroundTexture->setId(id); + m_opengl.setTextureLookup(id, m_backgroundTexture); + + m_backgroundImagePath = config.backgroundImagePath; + + qDebug() << "Background texture loaded:" << config.backgroundImagePath; + if (m_backgroundTexture) { + qDebug() << " Size:" << tex->width() << "x" << tex->height(); + qDebug() << " Created:" << tex->isCreated(); + } + } catch (const std::exception &e) { + qWarning() << "Failed to load background image:" << e.what(); + m_backgroundTexture.reset(); + m_backgroundImagePath.clear(); + } + } +} + +void MapCanvas::renderBackgroundImage() +{ + loadBackgroundImageIfNeeded(); + + auto &gl = getOpenGL(); + const auto &config = getConfig().canvas.advanced; + + if (!m_backgroundTexture || !config.useBackgroundImage) { + // No background image, use solid color + // Clear framebuffer first + gl.clear(Color{getConfig().canvas.backgroundColor}.withAlpha(1.0f)); + + // Render a full-screen opaque quad to ensure proper background color + // This prevents blending artifacts with map tiles + const Color bgColor = Color{getConfig().canvas.backgroundColor}.withAlpha(1.0f); + std::vector verts; + verts.reserve(4); + verts.emplace_back(bgColor, glm::vec3{-1.0f, -1.0f, 0.0f}); // Bottom-left + verts.emplace_back(bgColor, glm::vec3{+1.0f, -1.0f, 0.0f}); // Bottom-right + verts.emplace_back(bgColor, glm::vec3{+1.0f, +1.0f, 0.0f}); // Top-right + verts.emplace_back(bgColor, glm::vec3{-1.0f, +1.0f, 0.0f}); // Top-left + + // Set identity matrix for screen-space rendering + setMvp(glm::mat4{1.f}); + + // Render state: no blending, no depth test for background + auto state = GLRenderState{}; + state.blend = BlendModeEnum::NONE; // Opaque background + state.depth.reset(); // No depth testing + state.uniforms.color = Colors::white; + state.uniforms.enableRadialTransparency = false; + state.uniforms.texturesDisabled = true; // No textures for solid color + state.uniforms.isNight = false; + + // Render the opaque background quad + gl.renderColoredQuads(verts, state); + + // Restore viewport and matrices for 3D rendering + setViewportAndMvp(width(), height()); + return; + } + + // Clear to background color with full opacity + // Force alpha to 1.0 to ensure opaque background (no blending artifacts) + gl.clear(Color{getConfig().canvas.backgroundColor}.withAlpha(1.0f)); + + const auto texId = m_backgroundTexture->getId(); + const float opacity = config.backgroundOpacity; + const auto fitMode = static_cast(config.backgroundFitMode); + + // Get texture and viewport dimensions + auto *tex = m_backgroundTexture->get(); + const float texWidth = static_cast(tex->width()); + const float texHeight = static_cast(tex->height()); + const float viewWidth = static_cast(width()); + const float viewHeight = static_cast(height()); + + // Calculate quad vertices and UV coordinates based on fit mode + glm::vec3 quadMin{-1.f, -1.f, 0.0f}; // NDC coordinates + glm::vec3 quadMax{+1.f, +1.f, 0.0f}; + glm::vec2 uvMin{0.0f, 0.0f}; // Fixed: UV coordinates (0,0 = top-left) + glm::vec2 uvMax{1.0f, 1.0f}; // Fixed: (1,1 = bottom-right) + + switch (fitMode) { + case BackgroundFitModeEnum::FIT: { + // Scale uniformly to fit inside viewport (letterbox/pillarbox) + const float texAspect = texWidth / texHeight; + const float viewAspect = viewWidth / viewHeight; + if (texAspect > viewAspect) { + // Image is wider - pillarbox (black bars on top/bottom) + const float scale = viewAspect / texAspect; + quadMin.y = -scale; + quadMax.y = scale; + } else { + // Image is taller - letterbox (black bars on sides) + const float scale = texAspect / viewAspect; + quadMin.x = -scale; + quadMax.x = scale; + } + break; + } + case BackgroundFitModeEnum::FILL: { + // Scale uniformly to cover viewport (crop excess) + const float texAspect = texWidth / texHeight; + const float viewAspect = viewWidth / viewHeight; + if (texAspect > viewAspect) { + // Image is wider - crop left/right + const float uvWidth = viewAspect / texAspect; + const float uvOffset = (1.0f - uvWidth) * 0.5f; + uvMin.x = uvOffset; + uvMax.x = 1.0f - uvOffset; + } else { + // Image is taller - crop top/bottom + const float uvHeight = texAspect / viewAspect; + const float uvOffset = (1.0f - uvHeight) * 0.5f; + uvMin.y = uvOffset; + uvMax.y = 1.0f - uvOffset; + } + break; + } + case BackgroundFitModeEnum::STRETCH: + // Non-uniform scale to fill exactly (default, no changes needed) + break; + case BackgroundFitModeEnum::CENTER: { + // No scale, center in viewport + const float quadWidth = (texWidth / viewWidth) * 2.0f; // Convert to NDC + const float quadHeight = (texHeight / viewHeight) * 2.0f; + quadMin.x = -quadWidth * 0.5f; + quadMax.x = quadWidth * 0.5f; + quadMin.y = -quadHeight * 0.5f; + quadMax.y = quadHeight * 0.5f; + break; + } + case BackgroundFitModeEnum::TILE: { + // Repeat texture to fill viewport + uvMax.x = viewWidth / texWidth; + uvMax.y = viewHeight / texHeight; + break; + } + case BackgroundFitModeEnum::FOCUSED: { + // FOCUSED mode: Render in WORLD SPACE (like map tiles) + // This ensures background pans and zooms perfectly with the map + + const float focusScale = config.backgroundFocusedScale; + const float offsetX = config.backgroundFocusedOffsetX; + const float offsetY = config.backgroundFocusedOffsetY; + + // Texture aspect ratio + const float texAspect = texWidth / texHeight; + + // Base size in world units (how many tiles the texture covers) + // focusScale: larger values = bigger background = more world units covered + const float baseWorldSize = 100.0f * focusScale; + + // Calculate world-space quad size preserving aspect ratio + float worldWidth, worldHeight; + if (texAspect > 1.0f) { + // Wider texture + worldWidth = baseWorldSize; + worldHeight = baseWorldSize / texAspect; + } else { + // Taller texture + worldHeight = baseWorldSize; + worldWidth = baseWorldSize * texAspect; + } + + // Position background in world space + // Center of texture is at (offsetX, offsetY) in world coordinates + const float worldCenterX = offsetX; + const float worldCenterY = offsetY; + + // CRITICAL: Place background at CURRENT LAYER depth (eliminates parallax!) + // Slightly below (-0.01) so tiles always render on top via depth testing + const float worldZ = static_cast(m_currentLayer) - 0.01f; + + // Calculate world-space quad corners + quadMin.x = worldCenterX - (worldWidth * 0.5f); + quadMax.x = worldCenterX + (worldWidth * 0.5f); + quadMin.y = worldCenterY - (worldHeight * 0.5f); + quadMax.y = worldCenterY + (worldHeight * 0.5f); + quadMin.z = worldZ; + quadMax.z = worldZ; + + // Use full texture (no UV manipulation needed) + uvMin = glm::vec2{0.0f, 0.0f}; + uvMax = glm::vec2{1.0f, 1.0f}; + + // Create quad in WORLD SPACE + const Color white = Colors::white.withAlpha(opacity); + std::vector verts; + verts.reserve(4); + verts.emplace_back(white, glm::vec2{uvMin.x, uvMin.y}, glm::vec3{quadMin.x, quadMin.y, worldZ}); + verts.emplace_back(white, glm::vec2{uvMax.x, uvMin.y}, glm::vec3{quadMax.x, quadMin.y, worldZ}); + verts.emplace_back(white, glm::vec2{uvMax.x, uvMax.y}, glm::vec3{quadMax.x, quadMax.y, worldZ}); + verts.emplace_back(white, glm::vec2{uvMin.x, uvMax.y}, glm::vec3{quadMin.x, quadMax.y, worldZ}); + + // Use world-space rendering (same matrix as map tiles) + setViewportAndMvp(width(), height()); + + // Render state - DISABLE depth testing so background never occludes tiles + // Background renders first, then all tiles render on top with normal depth testing + auto state = GLRenderState{}; + state.blend = (opacity < 1.0f) ? BlendModeEnum::TRANSPARENCY : BlendModeEnum::NONE; + state.depth.reset(); // No depth testing - background is always "behind" everything + state.uniforms.color = Colors::white; + state.uniforms.enableRadialTransparency = false; // Don't apply layer effects + state.uniforms.texturesDisabled = false; + state.uniforms.isNight = false; + + // Render the world-space background quad + gl.renderColoredTexturedQuads(verts, state.withTexture0(texId)); + + // Early return - already rendered and set matrices + return; + } + } + + // Create fullscreen quad with calculated UV coordinates (for screen-space modes) + // Note: Texture is mirrored when loaded, so V coordinates are flipped + const Color white = Colors::white.withAlpha(opacity); + std::vector verts; + verts.reserve(4); + verts.emplace_back(white, glm::vec2{uvMin.x, uvMin.y}, glm::vec3{quadMin.x, quadMin.y, 0.0f}); // Bottom-left + verts.emplace_back(white, glm::vec2{uvMax.x, uvMin.y}, glm::vec3{quadMax.x, quadMin.y, 0.0f}); // Bottom-right + verts.emplace_back(white, glm::vec2{uvMax.x, uvMax.y}, glm::vec3{quadMax.x, quadMax.y, 0.0f}); // Top-right + verts.emplace_back(white, glm::vec2{uvMin.x, uvMax.y}, glm::vec3{quadMin.x, quadMax.y, 0.0f}); // Top-left + + // Set identity matrix for screen-space rendering (non-FOCUSED modes) + setMvp(glm::mat4{1.f}); + + // Simple render state + auto state = GLRenderState{}; + state.blend = (opacity < 1.0f) ? BlendModeEnum::TRANSPARENCY : BlendModeEnum::NONE; + state.depth.reset(); + state.uniforms.color = Colors::white; + state.uniforms.enableRadialTransparency = false; + state.uniforms.texturesDisabled = false; + state.uniforms.isNight = false; + + qDebug() << "Rendering background with texture ID:" << static_cast(texId); + + // Render the textured background + gl.renderColoredTexturedQuads(verts, state.withTexture0(texId)); + + // Restore viewport and matrices for 3D rendering + setViewportAndMvp(width(), height()); +} + void MapCanvas::actuallyPaintGL() { // DECL_TIMER(t, __FUNCTION__); - setViewportAndMvp(width(), height()); + // NOTE: setViewportAndMvp is called by renderBackgroundImage(), so we don't call it here + // to avoid clearing the background that was just rendered auto &gl = getOpenGL(); - gl.clear(Color{getConfig().canvas.backgroundColor}); + renderBackgroundImage(); // Render background or clear to solid color (also sets viewport/MVP) if (m_data.isEmpty()) { getGLFont().renderTextCentered("No map loaded"); @@ -777,14 +1049,13 @@ void MapCanvas::paintMap() return; } - // TODO: add a GUI indicator for pending update? renderMapBatches(); - if (pending) { - if (m_batches.pendingUpdateFlashState.tick()) { - const QString msg = "CAUTION: Async map update pending!"; - getGLFont().renderTextCentered(msg); - } + // Show a subtle indicator for pending async updates (less intrusive than the old CAUTION message) + if (pending && m_batches.pendingUpdateFlashState.tick()) { + // Just show a small "●" symbol - much less intrusive than the large CAUTION message + const QString msg = "●"; // Small circle indicator + getGLFont().renderTextCentered(msg); } } @@ -1018,13 +1289,16 @@ void MapCanvas::renderMapBatches() auto &gl = getOpenGL(); BatchedMeshes &batchedMeshes = batches.batchedMeshes; + // Player position is approximated by scroll position (camera center) + current layer Z + const glm::vec3 playerPos = glm::vec3(m_scroll.x, m_scroll.y, static_cast(m_currentLayer)); + const bool isNight = batches.isNight; const auto drawLayer = - [&batches, &batchedMeshes, wantExtraDetail, wantDoorNames](const int thisLayer, - const int currentLayer) { + [&batches, &batchedMeshes, &playerPos, isNight, wantExtraDetail, wantDoorNames](const int thisLayer, + const int currentLayer) { const auto it_mesh = batchedMeshes.find(thisLayer); if (it_mesh != batchedMeshes.end()) { LayerMeshes &meshes = it_mesh->second; - meshes.render(thisLayer, currentLayer); + meshes.render(thisLayer, currentLayer, playerPos, isNight); } if (wantExtraDetail) { diff --git a/src/mainwindow/mainwindow.cpp b/src/mainwindow/mainwindow.cpp index 939d3660f..1767f37b5 100644 --- a/src/mainwindow/mainwindow.cpp +++ b/src/mainwindow/mainwindow.cpp @@ -10,8 +10,11 @@ #include "../adventure/adventurewidget.h" #include "../adventure/xpstatuswidget.h" #include "../client/ClientWidget.h" +#include "../comms/CommsManager.h" +#include "../comms/CommsWidget.h" #include "../clock/mumeclock.h" #include "../clock/mumeclockwidget.h" +#include "../display/Filenames.h" #include "../display/InfomarkSelection.h" #include "../display/MapCanvasData.h" #include "../display/mapcanvas.h" @@ -32,6 +35,7 @@ #include "DescriptionWidget.h" #include "MapZoomSlider.h" #include "UpdateDialog.h" +#include "VisibilityFilterWidget.h" #include "aboutdialog.h" #include "findroomsdlg.h" #include "infomarkseditdlg.h" @@ -128,7 +132,11 @@ MainWindow::MainWindow() addApplicationFont(); registerMetatypes(); - m_mapData = new MapData(this); + // Create game observer and clock first, as MapData depends on them + m_gameObserver = std::make_unique(); + m_mumeClock = new MumeClock(getConfig().mumeClock.startEpoch, deref(m_gameObserver), this); + + m_mapData = new MapData(deref(m_mumeClock), this); MapData &mapData = deref(m_mapData); m_mapData->setObjectName("MapData"); @@ -145,9 +153,21 @@ MainWindow::MainWindow() m_pathMachine = new Mmapper2PathMachine(mapData, this); m_pathMachine->setObjectName("Mmapper2PathMachine"); - m_gameObserver = std::make_unique(); m_adventureTracker = new AdventureTracker(deref(m_gameObserver), this); + // Create AutoLogger early (needed by CommsWidget) + m_logger = new AutoLogger(this); + + // Communications Manager + m_commsManager = new CommsManager(this); + deref(m_gameObserver).sig2_sentToUserGmcp.connect(m_lifetime, [this](const GmcpMessage &gmcp) { + deref(m_commsManager).slot_parseGmcpInput(gmcp); + }); + deref(m_gameObserver).sig2_rawGameText.connect(m_lifetime, [this](const QString &text) { + deref(m_commsManager).slot_parseRawGameText(text); + }); + connect(m_commsManager, &CommsManager::sig_log, this, &MainWindow::slot_log); + // View -> Side Panels -> Client Panel m_clientWidget = new ClientWidget(this); m_clientWidget->setObjectName("InternalMudClientWidget"); @@ -166,7 +186,6 @@ MainWindow::MainWindow() m_dockDialogLog->setAllowedAreas(Qt::TopDockWidgetArea | Qt::BottomDockWidgetArea); m_dockDialogLog->setFeatures(QDockWidget::DockWidgetMovable | QDockWidget::DockWidgetFloatable | QDockWidget::DockWidgetClosable); - m_dockDialogLog->toggleViewAction()->setShortcut(tr("Ctrl+L")); addDockWidget(Qt::BottomDockWidgetArea, m_dockDialogLog); logWindow = new QTextBrowser(m_dockDialogLog); @@ -218,6 +237,18 @@ MainWindow::MainWindow() m_dockDialogAdventure->setWidget(m_adventureWidget); m_dockDialogAdventure->hide(); + // View -> Side Panels -> Communications Panel + m_dockDialogComms = new QDockWidget(tr("Communications"), this); + m_dockDialogComms->setObjectName("DockWidgetComms"); + m_dockDialogComms->setAllowedAreas(Qt::BottomDockWidgetArea | Qt::TopDockWidgetArea); + m_dockDialogComms->setFeatures(QDockWidget::DockWidgetClosable + | QDockWidget::DockWidgetFloatable + | QDockWidget::DockWidgetMovable); + addDockWidget(Qt::BottomDockWidgetArea, m_dockDialogComms); + m_commsWidget = new CommsWidget(deref(m_commsManager), m_logger, this); + m_dockDialogComms->setWidget(m_commsWidget); + m_dockDialogComms->hide(); + // View -> Side Panels -> Description / Area Panel m_descriptionWidget = new DescriptionWidget(this); m_dockDialogDescription = new QDockWidget(tr("Description Panel"), this); @@ -229,12 +260,40 @@ MainWindow::MainWindow() addDockWidget(Qt::RightDockWidgetArea, m_dockDialogDescription); m_dockDialogDescription->setWidget(m_descriptionWidget); - m_mumeClock = new MumeClock(getConfig().mumeClock.startEpoch, deref(m_gameObserver), this); + // View -> Toolbars -> Visibility Filter + m_visibilityFilterWidget = new VisibilityFilterWidget(this); + m_dockDialogVisibleMarkers = new QDockWidget(tr("Visibility Filter"), this); + m_dockDialogVisibleMarkers->setObjectName("DockWidgetVisibilityFilter"); + m_dockDialogVisibleMarkers->setAllowedAreas(Qt::AllDockWidgetAreas); + m_dockDialogVisibleMarkers->setFeatures(QDockWidget::DockWidgetMovable + | QDockWidget::DockWidgetFloatable + | QDockWidget::DockWidgetClosable); + addDockWidget(Qt::RightDockWidgetArea, m_dockDialogVisibleMarkers); + m_dockDialogVisibleMarkers->setWidget(m_visibilityFilterWidget); + m_dockDialogVisibleMarkers->hide(); + + // Connect visibility filter changes to map update + // Separate signals ensure we only rebuild what's necessary: + // - Infomarks visibility -> only rebuild infomark meshes + // - Connections visibility -> only rebuild map/connection batches + connect(m_visibilityFilterWidget, &VisibilityFilterWidget::sig_visibilityChanged, + this, [this]() { + m_mapWindow->getCanvas()->infomarksChanged(); + }); + + connect(m_visibilityFilterWidget, &VisibilityFilterWidget::sig_connectionsVisibilityChanged, + this, [this]() { + // Just trigger a repaint - connections use alpha transparency so no batch rebuild needed + m_mapWindow->getCanvas()->update(); + }); + if constexpr (!NO_UPDATER) { m_updateDialog = new UpdateDialog(this); } createActions(); + applyHotkeys(); + registerGlobalShortcuts(); setupToolBars(); setupMenuBar(); setupStatusBar(); @@ -244,8 +303,6 @@ MainWindow::MainWindow() setCorner(Qt::TopRightCorner, Qt::TopDockWidgetArea); setCorner(Qt::BottomRightCorner, Qt::BottomDockWidgetArea); - m_logger = new AutoLogger(this); - // TODO move this connect() wiring into AutoLogger::ctor ? GameObserver &observer = deref(m_gameObserver); observer.sig2_connected.connect(m_lifetime, [this]() { @@ -294,6 +351,8 @@ MainWindow::MainWindow() setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); } + radialTransparencyAct->setChecked(getConfig().canvas.enableRadialTransparency); + showStatusBarAct->setChecked(getConfig().general.showStatusBar); slot_setShowStatusBar(); @@ -483,6 +542,13 @@ void MainWindow::wireConnections() connect(m_mapData, &MapFrontend::sig_clearingMap, m_groupWidget, &GroupWidget::slot_mapUnloaded); connect(m_mumeClock, &MumeClock::sig_log, this, &MainWindow::slot_log); + connect(m_mumeClock, &MumeClock::sig_seasonChanged, m_mapWindow->getCanvas(), &MapCanvas::slot_onSeasonChanged); + + // Initialize the current season for seasonal textures + // This sets the global season variable so textures load correctly on startup + // Note: We only call setCurrentSeason() here, NOT slot_onSeasonChanged() + // because the OpenGL context isn't ready yet during construction + setCurrentSeason(m_mumeClock->getMumeMoment().toSeason()); connect(m_listener, &ConnectionListener::sig_log, this, &MainWindow::slot_log); connect(m_dockDialogClient, @@ -534,21 +600,18 @@ void MainWindow::createActions() openAct = new QAction(QIcon::fromTheme("document-open", QIcon(":/icons/open.png")), tr("&Open..."), this); - openAct->setShortcut(tr("Ctrl+O")); openAct->setStatusTip(tr("Open an existing file")); connect(openAct, &QAction::triggered, this, &MainWindow::slot_open); reloadAct = new QAction(QIcon::fromTheme("document-open-recent", QIcon(":/icons/reload.png")), tr("&Reload"), this); - reloadAct->setShortcut(tr("Ctrl+R")); reloadAct->setStatusTip(tr("Reload the current map")); connect(reloadAct, &QAction::triggered, this, &MainWindow::slot_reload); saveAct = new QAction(QIcon::fromTheme("document-save", QIcon(":/icons/save.png")), tr("&Save"), this); - saveAct->setShortcut(tr("Ctrl+S")); saveAct->setStatusTip(tr("Save the document to disk")); saveAct->setEnabled(false); connect(saveAct, &QAction::triggered, this, &MainWindow::slot_save); @@ -578,19 +641,16 @@ void MainWindow::createActions() connect(mergeAct, &QAction::triggered, this, &MainWindow::slot_merge); exitAct = new QAction(QIcon::fromTheme("application-exit"), tr("E&xit"), this); - exitAct->setShortcut(tr("Ctrl+Q")); exitAct->setStatusTip(tr("Exit the application")); connect(exitAct, &QAction::triggered, this, &QWidget::close); m_undoAction = new QAction(QIcon::fromTheme("edit-undo"), tr("&Undo"), this); - m_undoAction->setShortcut(QKeySequence::Undo); m_undoAction->setStatusTip(tr("Undo the last action")); connect(m_undoAction, &QAction::triggered, m_mapData, &MapData::slot_undo); connect(m_mapData, &MapData::sig_undoAvailable, m_undoAction, &QAction::setEnabled); m_undoAction->setEnabled(false); m_redoAction = new QAction(QIcon::fromTheme("edit-redo"), tr("&Redo"), this); - m_redoAction->setShortcut(QKeySequence::Redo); m_redoAction->setStatusTip(tr("Redo the last undone action")); connect(m_redoAction, &QAction::triggered, m_mapData, &MapData::slot_redo); connect(m_mapData, &MapData::sig_redoAvailable, m_redoAction, &QAction::setEnabled); @@ -600,7 +660,6 @@ void MainWindow::createActions() QIcon(":/icons/preferences.png")), tr("&Preferences"), this); - preferencesAct->setShortcut(tr("Ctrl+P")); preferencesAct->setStatusTip(tr("MMapper preferences")); connect(preferencesAct, &QAction::triggered, this, &MainWindow::slot_onPreferences); @@ -647,22 +706,23 @@ void MainWindow::createActions() tr("Zoom In"), this); zoomInAct->setStatusTip(tr("Zooms In current map")); - zoomInAct->setShortcut(tr("Ctrl++")); zoomOutAct = new QAction(QIcon::fromTheme("zoom-out", QIcon(":/icons/viewmag-.png")), tr("Zoom Out"), this); - zoomOutAct->setShortcut(tr("Ctrl+-")); zoomOutAct->setStatusTip(tr("Zooms Out current map")); zoomResetAct = new QAction(QIcon::fromTheme("zoom-original", QIcon(":/icons/viewmagfit.png")), tr("Zoom Reset"), this); - zoomResetAct->setShortcut(tr("Ctrl+0")); zoomResetAct->setStatusTip(tr("Zoom to original size")); alwaysOnTopAct = new QAction(tr("Always On Top"), this); alwaysOnTopAct->setCheckable(true); connect(alwaysOnTopAct, &QAction::triggered, this, &MainWindow::slot_alwaysOnTop); + radialTransparencyAct = new QAction(tr("Radial Transparency"), this); + radialTransparencyAct->setCheckable(true); + connect(radialTransparencyAct, &QAction::triggered, this, &MainWindow::slot_setRadialTransparency); + showStatusBarAct = new QAction(tr("Always Show Status Bar"), this); showStatusBarAct->setCheckable(true); connect(showStatusBarAct, &QAction::triggered, this, &MainWindow::slot_setShowStatusBar); @@ -680,26 +740,11 @@ void MainWindow::createActions() layerUpAct = new QAction(QIcon::fromTheme("go-up", QIcon(":/icons/layerup.png")), tr("Layer Up"), this); - layerUpAct->setShortcut(tr([]() -> const char * { - // Technically tr() could convert Ctrl to Meta, right? - if constexpr (CURRENT_PLATFORM == PlatformEnum::Mac) { - return "Meta+Tab"; - } - return "Ctrl+Tab"; - }())); layerUpAct->setStatusTip(tr("Layer Up")); connect(layerUpAct, &QAction::triggered, this, &MainWindow::slot_onLayerUp); layerDownAct = new QAction(QIcon::fromTheme("go-down", QIcon(":/icons/layerdown.png")), tr("Layer Down"), this); - - layerDownAct->setShortcut(tr([]() -> const char * { - // Technically tr() could convert Ctrl to Meta, right? - if constexpr (CURRENT_PLATFORM == PlatformEnum::Mac) { - return "Meta+Shift+Tab"; - } - return "Ctrl+Shift+Tab"; - }())); layerDownAct->setStatusTip(tr("Layer Down")); connect(layerDownAct, &QAction::triggered, this, &MainWindow::slot_onLayerDown); @@ -816,7 +861,6 @@ void MainWindow::createActions() tr("Edit Selected Rooms"), this); editRoomSelectionAct->setStatusTip(tr("Edit Selected Rooms")); - editRoomSelectionAct->setShortcut(tr("Ctrl+E")); connect(editRoomSelectionAct, &QAction::triggered, this, &MainWindow::slot_onEditRoomSelection); deleteRoomSelectionAct = new QAction(QIcon(":/icons/roomdelete.png"), @@ -871,7 +915,6 @@ void MainWindow::createActions() findRoomsAct = new QAction(QIcon(":/icons/roomfind.png"), tr("&Find Rooms"), this); findRoomsAct->setStatusTip(tr("Find matching rooms")); - findRoomsAct->setShortcut(tr("Ctrl+F")); connect(findRoomsAct, &QAction::triggered, this, &MainWindow::slot_onFindRoom); clientAct = new QAction(QIcon(":/icons/online.png"), tr("&Launch mud client"), this); @@ -884,6 +927,12 @@ void MainWindow::createActions() connect(saveLogAct, &QAction::triggered, m_clientWidget, &ClientWidget::slot_saveLog); saveLogAct->setStatusTip(tr("Save log as file")); + saveCommsLogAct = new QAction(QIcon::fromTheme("document-save", QIcon(":/icons/save.png")), + tr("Save &communications log as..."), + this); + connect(saveCommsLogAct, &QAction::triggered, m_commsWidget, &CommsWidget::slot_saveLog); + saveCommsLogAct->setStatusTip(tr("Save communications log as file")); + releaseAllPathsAct = new QAction(QIcon(":/icons/cancel.png"), tr("Release All Paths"), this); releaseAllPathsAct->setStatusTip(tr("Release all paths")); releaseAllPathsAct->setCheckable(false); @@ -1005,6 +1054,104 @@ void MainWindow::createActions() connect(rebuildMeshesAct, &QAction::triggered, getCanvas(), &MapCanvas::slot_rebuildMeshes); } +void MainWindow::registerGlobalShortcuts() +{ + // Register all actions with the main window so their shortcuts work globally + // This is required for keyboard shortcuts to work anywhere in the application + + // File operations + addAction(newAct); + addAction(openAct); + addAction(mergeAct); + addAction(reloadAct); + addAction(saveAct); + addAction(saveAsAct); + addAction(exportBaseMapAct); + addAction(exportMm2xmlMapAct); + addAction(exportWebMapAct); + addAction(exportMmpMapAct); + addAction(exitAct); + + // Edit operations + addAction(m_undoAction); + addAction(m_redoAction); + addAction(preferencesAct); + addAction(findRoomsAct); + addAction(editRoomSelectionAct); + + // View operations + addAction(zoomInAct); + addAction(zoomOutAct); + addAction(zoomResetAct); + addAction(layerUpAct); + addAction(layerDownAct); + addAction(layerResetAct); + + // View toggles + addAction(radialTransparencyAct); + addAction(showStatusBarAct); + addAction(showScrollBarsAct); + addAction(showMenuBarAct); + addAction(alwaysOnTopAct); + + // Side panels + if (m_dockDialogLog && m_dockDialogLog->toggleViewAction()) { + addAction(m_dockDialogLog->toggleViewAction()); + } + if (m_dockDialogClient && m_dockDialogClient->toggleViewAction()) { + addAction(m_dockDialogClient->toggleViewAction()); + } + if (m_dockDialogGroup && m_dockDialogGroup->toggleViewAction()) { + addAction(m_dockDialogGroup->toggleViewAction()); + } + if (m_dockDialogRoom && m_dockDialogRoom->toggleViewAction()) { + addAction(m_dockDialogRoom->toggleViewAction()); + } + if (m_dockDialogAdventure && m_dockDialogAdventure->toggleViewAction()) { + addAction(m_dockDialogAdventure->toggleViewAction()); + } + if (m_dockDialogComms && m_dockDialogComms->toggleViewAction()) { + addAction(m_dockDialogComms->toggleViewAction()); + } + if (m_dockDialogDescription && m_dockDialogDescription->toggleViewAction()) { + addAction(m_dockDialogDescription->toggleViewAction()); + } + + // Mouse modes + addAction(mouseMode.modeMoveSelectAct); + addAction(mouseMode.modeRoomRaypickAct); + addAction(mouseMode.modeRoomSelectAct); + addAction(mouseMode.modeConnectionSelectAct); + addAction(mouseMode.modeInfomarkSelectAct); + addAction(mouseMode.modeCreateInfomarkAct); + addAction(mouseMode.modeCreateRoomAct); + addAction(mouseMode.modeCreateConnectionAct); + addAction(mouseMode.modeCreateOnewayConnectionAct); + + // Room operations + addAction(createRoomAct); + addAction(moveUpRoomSelectionAct); + addAction(moveDownRoomSelectionAct); + addAction(mergeUpRoomSelectionAct); + addAction(mergeDownRoomSelectionAct); + addAction(deleteRoomSelectionAct); + addAction(connectToNeighboursRoomSelectionAct); + addAction(gotoRoomAct); + addAction(forceRoomAct); + + // Connection operations + addAction(deleteConnectionSelectionAct); + + // Infomark operations + addAction(infomarkActions.deleteInfomarkAct); + addAction(infomarkActions.editInfomarkAct); + + // Other + addAction(rebuildMeshesAct); + + qDebug() << "Registered all actions with main window for global shortcuts"; +} + static void setConfigMapMode(const MapModeEnum mode) { setConfig().general.mapMode = mode; @@ -1156,12 +1303,14 @@ void MainWindow::setupMenuBar() toolbars->addAction(roomToolBar->toggleViewAction()); toolbars->addAction(connectionToolBar->toggleViewAction()); toolbars->addAction(settingsToolBar->toggleViewAction()); + toolbars->addAction(m_dockDialogVisibleMarkers->toggleViewAction()); QMenu *sidepanels = viewMenu->addMenu(tr("&Side Panels")); sidepanels->addAction(m_dockDialogLog->toggleViewAction()); sidepanels->addAction(m_dockDialogClient->toggleViewAction()); sidepanels->addAction(m_dockDialogGroup->toggleViewAction()); sidepanels->addAction(m_dockDialogRoom->toggleViewAction()); sidepanels->addAction(m_dockDialogAdventure->toggleViewAction()); + sidepanels->addAction(m_dockDialogComms->toggleViewAction()); sidepanels->addAction(m_dockDialogDescription->toggleViewAction()); viewMenu->addSeparator(); viewMenu->addAction(zoomInAct); @@ -1174,6 +1323,8 @@ void MainWindow::setupMenuBar() viewMenu->addSeparator(); viewMenu->addAction(rebuildMeshesAct); viewMenu->addSeparator(); + viewMenu->addAction(radialTransparencyAct); + viewMenu->addAction(showStatusBarAct); viewMenu->addAction(showScrollBarsAct); if constexpr (CURRENT_PLATFORM != PlatformEnum::Mac) { @@ -1186,6 +1337,8 @@ void MainWindow::setupMenuBar() tr("&Integrated Mud Client")); clientMenu->addAction(clientAct); clientMenu->addAction(saveLogAct); + clientMenu->addSeparator(); + clientMenu->addAction(saveCommsLogAct); QMenu *pathMachineMenu = settingsMenu->addMenu(QIcon(":/icons/goto.png"), tr("&Path Machine")); pathMachineMenu->addAction(mouseMode.modeRoomSelectAct); pathMachineMenu->addSeparator(); @@ -1269,6 +1422,15 @@ void MainWindow::slot_alwaysOnTop() show(); } +void MainWindow::slot_setRadialTransparency() +{ + const bool enableRadialTransparency = this->radialTransparencyAct->isChecked(); + setConfig().canvas.enableRadialTransparency = enableRadialTransparency; + if (m_mapWindow) { + m_mapWindow->update(); + } +} + void MainWindow::slot_setShowStatusBar() { const bool showStatusBar = this->showStatusBarAct->isChecked(); @@ -1414,10 +1576,22 @@ void MainWindow::slot_onPreferences() &ConfigDialog::sig_graphicsSettingsChanged, m_mapWindow, &MapWindow::slot_graphicsSettingsChanged); + connect(m_configDialog.get(), + &ConfigDialog::sig_textureSettingsChanged, + m_mapWindow->getCanvas(), + &MapCanvas::slot_reloadTextures); connect(m_configDialog.get(), &ConfigDialog::sig_groupSettingsChanged, m_groupManager, &Mmapper2Group::slot_groupSettingsChanged); + connect(m_configDialog.get(), + &ConfigDialog::sig_commsSettingsChanged, + m_commsWidget, + &CommsWidget::slot_loadSettings); + connect(m_configDialog.get(), + &ConfigDialog::sig_hotkeysChanged, + this, + &MainWindow::applyHotkeys); m_configDialog->show(); } @@ -1478,6 +1652,12 @@ bool MainWindow::eventFilter(QObject *const obj, QEvent *const event) void MainWindow::closeEvent(QCloseEvent *const event) { // REVISIT: wait and see if we're actually exiting first? + + // Save communications log if enabled + if (m_commsWidget) { + m_commsWidget->slot_saveLogOnExit(); + } + writeSettings(); if (!maybeSave()) { @@ -2131,3 +2311,107 @@ void MainWindow::onSuccessfulSave(const SaveModeEnum mode, } } } + +void MainWindow::applyHotkeys() +{ + const auto &hotkeys = getConfig().hotkeys; + + qDebug() << "=== Applying hotkeys ==="; + + // Helper lambda to apply shortcut only if not empty + auto applyShortcut = [](QAction *action, const QString &shortcut) { + if (action && !shortcut.isEmpty()) { + action->setShortcut(QKeySequence(shortcut)); + qDebug() << " Setting shortcut:" << action->text() << "=" << shortcut; + } else if (action) { + action->setShortcut(QKeySequence()); + qDebug() << " Clearing shortcut:" << action->text(); + } + }; + + // File operations + applyShortcut(openAct, hotkeys.fileOpen.get()); + applyShortcut(saveAct, hotkeys.fileSave.get()); + applyShortcut(reloadAct, hotkeys.fileReload.get()); + applyShortcut(exitAct, hotkeys.fileQuit.get()); + + // Edit operations + applyShortcut(m_undoAction, hotkeys.editUndo.get()); + applyShortcut(m_redoAction, hotkeys.editRedo.get()); + applyShortcut(preferencesAct, hotkeys.editPreferences.get()); + applyShortcut(findRoomsAct, hotkeys.editFindRooms.get()); + applyShortcut(editRoomSelectionAct, hotkeys.editRoom.get()); + + // View operations + applyShortcut(zoomInAct, hotkeys.viewZoomIn.get()); + applyShortcut(zoomOutAct, hotkeys.viewZoomOut.get()); + applyShortcut(zoomResetAct, hotkeys.viewZoomReset.get()); + applyShortcut(layerUpAct, hotkeys.viewLayerUp.get()); + applyShortcut(layerDownAct, hotkeys.viewLayerDown.get()); + applyShortcut(layerResetAct, hotkeys.viewLayerReset.get()); + + // View toggles + applyShortcut(radialTransparencyAct, hotkeys.viewRadialTransparency.get()); + applyShortcut(showStatusBarAct, hotkeys.viewStatusBar.get()); + applyShortcut(showScrollBarsAct, hotkeys.viewScrollBars.get()); + applyShortcut(showMenuBarAct, hotkeys.viewMenuBar.get()); + applyShortcut(alwaysOnTopAct, hotkeys.viewAlwaysOnTop.get()); + + // Side panels + if (m_dockDialogLog && m_dockDialogLog->toggleViewAction()) { + applyShortcut(m_dockDialogLog->toggleViewAction(), hotkeys.panelLog.get()); + } + if (m_dockDialogClient && m_dockDialogClient->toggleViewAction()) { + applyShortcut(m_dockDialogClient->toggleViewAction(), hotkeys.panelClient.get()); + } + if (m_dockDialogGroup && m_dockDialogGroup->toggleViewAction()) { + applyShortcut(m_dockDialogGroup->toggleViewAction(), hotkeys.panelGroup.get()); + } + if (m_dockDialogRoom && m_dockDialogRoom->toggleViewAction()) { + applyShortcut(m_dockDialogRoom->toggleViewAction(), hotkeys.panelRoom.get()); + } + if (m_dockDialogAdventure && m_dockDialogAdventure->toggleViewAction()) { + applyShortcut(m_dockDialogAdventure->toggleViewAction(), hotkeys.panelAdventure.get()); + } + if (m_dockDialogComms && m_dockDialogComms->toggleViewAction()) { + applyShortcut(m_dockDialogComms->toggleViewAction(), hotkeys.panelComms.get()); + } + if (m_dockDialogDescription && m_dockDialogDescription->toggleViewAction()) { + applyShortcut(m_dockDialogDescription->toggleViewAction(), hotkeys.panelDescription.get()); + } + + // Mouse modes + applyShortcut(mouseMode.modeMoveSelectAct, hotkeys.modeMoveMap.get()); + applyShortcut(mouseMode.modeRoomRaypickAct, hotkeys.modeRaypick.get()); + applyShortcut(mouseMode.modeRoomSelectAct, hotkeys.modeSelectRooms.get()); + applyShortcut(mouseMode.modeInfomarkSelectAct, hotkeys.modeSelectMarkers.get()); + applyShortcut(mouseMode.modeConnectionSelectAct, hotkeys.modeSelectConnection.get()); + applyShortcut(mouseMode.modeCreateInfomarkAct, hotkeys.modeCreateMarker.get()); + applyShortcut(mouseMode.modeCreateRoomAct, hotkeys.modeCreateRoom.get()); + applyShortcut(mouseMode.modeCreateConnectionAct, hotkeys.modeCreateConnection.get()); + applyShortcut(mouseMode.modeCreateOnewayConnectionAct, hotkeys.modeCreateOnewayConnection.get()); + + // Room operations + applyShortcut(createRoomAct, hotkeys.roomCreate.get()); + applyShortcut(moveUpRoomSelectionAct, hotkeys.roomMoveUp.get()); + applyShortcut(moveDownRoomSelectionAct, hotkeys.roomMoveDown.get()); + applyShortcut(mergeUpRoomSelectionAct, hotkeys.roomMergeUp.get()); + applyShortcut(mergeDownRoomSelectionAct, hotkeys.roomMergeDown.get()); + applyShortcut(deleteRoomSelectionAct, hotkeys.roomDelete.get()); + applyShortcut(connectToNeighboursRoomSelectionAct, hotkeys.roomConnectNeighbors.get()); + applyShortcut(gotoRoomAct, hotkeys.roomMoveToSelected.get()); + applyShortcut(forceRoomAct, hotkeys.roomUpdateSelected.get()); + + // Apply alternative preferences shortcut (Esc) + if (preferencesAct && !hotkeys.editPreferencesAlt.get().isEmpty()) { + QList shortcuts; + if (!hotkeys.editPreferences.get().isEmpty()) { + shortcuts << QKeySequence(hotkeys.editPreferences.get()); + } + shortcuts << QKeySequence(hotkeys.editPreferencesAlt.get()); + preferencesAct->setShortcuts(shortcuts); + qDebug() << " Setting dual shortcuts for Preferences:" << shortcuts; + } + + qDebug() << "=== Hotkeys applied ==="; +} diff --git a/src/mainwindow/mainwindow.h b/src/mainwindow/mainwindow.h index 674860557..42603a50d 100644 --- a/src/mainwindow/mainwindow.h +++ b/src/mainwindow/mainwindow.h @@ -34,6 +34,8 @@ class AdventureTracker; class AdventureWidget; class AutoLogger; class ClientWidget; +class CommsManager; +class CommsWidget; class ConfigDialog; class ConnectionListener; class ConnectionSelection; @@ -66,6 +68,7 @@ class RoomSelection; class RoomWidget; class UpdateDialog; class DescriptionWidget; +class VisibilityFilterWidget; struct MapLoadData; @@ -85,6 +88,8 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow QDockWidget *m_dockDialogGroup = nullptr; QDockWidget *m_dockDialogAdventure = nullptr; QDockWidget *m_dockDialogDescription = nullptr; + QDockWidget *m_dockDialogComms = nullptr; + QDockWidget *m_dockDialogVisibleMarkers = nullptr; std::unique_ptr m_gameObserver; AutoLogger *m_logger = nullptr; @@ -108,7 +113,11 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow AdventureTracker *m_adventureTracker = nullptr; AdventureWidget *m_adventureWidget = nullptr; + CommsManager *m_commsManager = nullptr; + CommsWidget *m_commsWidget = nullptr; + DescriptionWidget *m_descriptionWidget = nullptr; + VisibilityFilterWidget *m_visibilityFilterWidget = nullptr; SharedRoomSelection m_roomSelection; std::shared_ptr m_connectionSelection; @@ -166,6 +175,7 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow QAction *zoomOutAct = nullptr; QAction *zoomResetAct = nullptr; QAction *alwaysOnTopAct = nullptr; + QAction *radialTransparencyAct = nullptr; QAction *showStatusBarAct = nullptr; QAction *showScrollBarsAct = nullptr; QAction *showMenuBarAct = nullptr; @@ -222,6 +232,7 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow QAction *clientAct = nullptr; QAction *saveLogAct = nullptr; + QAction *saveCommsLogAct = nullptr; QAction *gotoRoomAct = nullptr; QAction *forceRoomAct = nullptr; @@ -304,6 +315,8 @@ class NODISCARD_QOBJECT MainWindow final : public QMainWindow void setupMenuBar(); void setupToolBars(); void setupStatusBar(); + void applyHotkeys(); + void registerGlobalShortcuts(); void readSettings(); void writeSettings(); @@ -432,6 +445,7 @@ public slots: void slot_onOfflineMode(); void slot_setMode(MapModeEnum mode); void slot_alwaysOnTop(); + void slot_setRadialTransparency(); void slot_setShowStatusBar(); void slot_setShowScrollBars(); void slot_setShowMenuBar(); diff --git a/src/opengl/OpenGL.cpp b/src/opengl/OpenGL.cpp index e097ae279..204db730e 100644 --- a/src/opengl/OpenGL.cpp +++ b/src/opengl/OpenGL.cpp @@ -25,6 +25,7 @@ #include #ifdef WIN32 +#include extern "C" { // Prefer discrete nVidia and AMD GPUs by default on Windows __declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001; diff --git a/src/opengl/OpenGLTypes.h b/src/opengl/OpenGLTypes.h index e5726972b..056317a49 100644 --- a/src/opengl/OpenGLTypes.h +++ b/src/opengl/OpenGLTypes.h @@ -87,6 +87,16 @@ struct NODISCARD FontVert3d final enum class NODISCARD DrawModeEnum { INVALID = 0, POINTS = 1, LINES = 2, TRIANGLES = 3, QUADS = 4 }; +// Background image fit modes (similar to 3D compositing software) +enum class NODISCARD BackgroundFitModeEnum { + FIT = 0, // Scale uniformly to fit inside viewport (letterbox/pillarbox) + FILL = 1, // Scale uniformly to cover viewport (crop excess) + STRETCH = 2, // Non-uniform scale to fill exactly + CENTER = 3, // No scale, center in viewport + TILE = 4, // Repeat texture + FOCUSED = 5 // Follow player position with adjustable scale +}; + struct NODISCARD LineParams final { float width = 1.f; @@ -190,6 +200,12 @@ struct NODISCARD GLRenderState final // glEnable(TEXTURE_2D), or glEnable(TEXTURE_3D) Textures textures; std::optional pointSize; + // For radial transparency around player on upper layers + glm::vec3 playerPos = glm::vec3(0.f, 0.f, 0.f); + int currentLayer = 0; + bool enableRadialTransparency = false; + bool texturesDisabled = false; + bool isNight = false; // True if current time is night (for darker tinting) }; Uniforms uniforms; diff --git a/src/opengl/legacy/AbstractShaderProgram.cpp b/src/opengl/legacy/AbstractShaderProgram.cpp index dc75f9b26..93f85677b 100644 --- a/src/opengl/legacy/AbstractShaderProgram.cpp +++ b/src/opengl/legacy/AbstractShaderProgram.cpp @@ -97,6 +97,15 @@ void AbstractShaderProgram::setUniform1fv(const GLint location, deref(functions).glUniform1fv(location, count, value); } +void AbstractShaderProgram::setUniform3fv(const GLint location, + const GLsizei count, + const GLfloat *const value) +{ + assert(m_isBound); + auto functions = m_functions.lock(); + deref(functions).glUniform3fv(location, count, value); +} + void AbstractShaderProgram::setUniform4fv(const GLint location, const GLsizei count, const GLfloat *const value) @@ -167,4 +176,16 @@ void AbstractShaderProgram::setViewport(const char *const name, const Viewport & setUniform4iv(location, 1, glm::value_ptr(viewport)); } +void AbstractShaderProgram::setVec3(const char *const name, const glm::vec3 &v) +{ + const auto location = getUniformLocation(name); + setUniform3fv(location, 1, glm::value_ptr(v)); +} + +void AbstractShaderProgram::setInt(const char *const name, const int value) +{ + const GLint location = getUniformLocation(name); + setUniform1iv(location, 1, &value); +} + } // namespace Legacy diff --git a/src/opengl/legacy/AbstractShaderProgram.h b/src/opengl/legacy/AbstractShaderProgram.h index 7113f5484..e9efd5ed4 100644 --- a/src/opengl/legacy/AbstractShaderProgram.h +++ b/src/opengl/legacy/AbstractShaderProgram.h @@ -73,6 +73,7 @@ struct NODISCARD AbstractShaderProgram public: void setUniform1iv(GLint location, GLsizei count, const GLint *value); void setUniform1fv(GLint location, GLsizei count, const GLfloat *value); + void setUniform3fv(GLint location, GLsizei count, const GLfloat *value); void setUniform4fv(GLint location, GLsizei count, const GLfloat *value); void setUniform4iv(GLint location, GLsizei count, const GLint *value); void setUniformMatrix4fv(GLint location, @@ -89,6 +90,8 @@ struct NODISCARD AbstractShaderProgram void setMatrix(const char *name, const glm::mat4 &m); void setTexture(const char *name, int textureUnit); void setViewport(const char *name, const Viewport &input_viewport); + void setVec3(const char *name, const glm::vec3 &v); + void setInt(const char *name, int value); }; } // namespace Legacy diff --git a/src/preferences/graphicspage.cpp b/src/preferences/graphicspage.cpp index 905eb8bda..78b7f5ac6 100644 --- a/src/preferences/graphicspage.cpp +++ b/src/preferences/graphicspage.cpp @@ -90,6 +90,15 @@ GraphicsPage::GraphicsPage(QWidget *parent) } }); + connect(ui->tileSetComboBox, + qOverload(&QComboBox::currentIndexChanged), + this, + &GraphicsPage::slot_textureSetChanged); + connect(ui->enableSeasonalTilesCheckBox, + &QCheckBox::stateChanged, + this, + &GraphicsPage::slot_enableSeasonalTexturesStateChanged); + connect(m_advanced.get(), &AdvancedGraphicsGroupBox::sig_graphicsSettingsChanged, this, @@ -119,8 +128,19 @@ void GraphicsPage::slot_loadConfig() ui->drawNeedsUpdate->setChecked(settings.showMissingMapId.get()); ui->drawNotMappedExits->setChecked(settings.showUnmappedExits.get()); ui->drawDoorNames->setChecked(settings.drawDoorNames); + ui->drawUpperLayersTextured->setChecked(settings.drawUpperLayersTextured); ui->resourceLineEdit->setText(settings.resourcesDirectory); + + // Block signals to prevent texture reload when just loading config + ui->tileSetComboBox->blockSignals(true); + ui->enableSeasonalTilesCheckBox->blockSignals(true); + + ui->tileSetComboBox->setCurrentIndex(static_cast(settings.textureSet)); + ui->enableSeasonalTilesCheckBox->setChecked(settings.enableSeasonalTextures); + + ui->tileSetComboBox->blockSignals(false); + ui->enableSeasonalTilesCheckBox->blockSignals(false); } void GraphicsPage::changeColorClicked(XNamedColor &namedColor, QPushButton *const pushButton) @@ -168,3 +188,31 @@ void GraphicsPage::slot_drawUpperLayersTexturedStateChanged(int /*unused*/) setConfig().canvas.drawUpperLayersTextured = ui->drawUpperLayersTextured->isChecked(); graphicsSettingsChanged(); } + +void GraphicsPage::slot_textureSetChanged(int index) +{ + auto &config = setConfig().canvas; + switch (index) { + case 0: + config.textureSet = TextureSetEnum::CLASSIC; + break; + case 1: + config.textureSet = TextureSetEnum::MODERN; + break; + case 2: + config.textureSet = TextureSetEnum::CUSTOM; + break; + default: + config.textureSet = TextureSetEnum::MODERN; + break; + } + graphicsSettingsChanged(); + emit sig_textureSettingsChanged(); +} + +void GraphicsPage::slot_enableSeasonalTexturesStateChanged(int /*unused*/) +{ + setConfig().canvas.enableSeasonalTextures = ui->enableSeasonalTilesCheckBox->isChecked(); + graphicsSettingsChanged(); + emit sig_textureSettingsChanged(); +} diff --git a/src/preferences/graphicspage.h b/src/preferences/graphicspage.h index 78719057d..19046dbce 100644 --- a/src/preferences/graphicspage.h +++ b/src/preferences/graphicspage.h @@ -37,6 +37,7 @@ class NODISCARD_QOBJECT GraphicsPage final : public QWidget signals: void sig_graphicsSettingsChanged(); + void sig_textureSettingsChanged(); public slots: void slot_loadConfig(); @@ -46,6 +47,8 @@ public slots: void slot_drawNotMappedExitsStateChanged(int); void slot_drawDoorNamesStateChanged(int); void slot_drawUpperLayersTexturedStateChanged(int); + void slot_textureSetChanged(int index); + void slot_enableSeasonalTexturesStateChanged(int state); // this slot just calls the signal... not useful void slot_graphicsSettingsChanged() { graphicsSettingsChanged(); } }; diff --git a/src/preferences/graphicspage.ui b/src/preferences/graphicspage.ui index 789f1ae28..d3d510ba3 100644 --- a/src/preferences/graphicspage.ui +++ b/src/preferences/graphicspage.ui @@ -300,17 +300,81 @@ - Modding + Tiles - - + + + 9 + + + 9 + + + 9 + + + 9 + + + 6 + + + + + Tile Set: + + + tileSetComboBox + + + + + + + + Classic + + + + + Modern + + + + + Custom + + + + + + + + Enable Seasonal Tiles + + + Automatically switch Tiles based on MUME in-game season + + + + + + + Custom Resource Path: + + + resourceLineEdit + + + + Path to resources - + Select diff --git a/src/resources/background/Arda_merp_bg.jpg b/src/resources/background/Arda_merp_bg.jpg new file mode 100644 index 000000000..35a4a7005 Binary files /dev/null and b/src/resources/background/Arda_merp_bg.jpg differ diff --git a/src/resources/background/MUME_edit_map.jpg b/src/resources/background/MUME_edit_map.jpg new file mode 100644 index 000000000..7df4ab136 Binary files /dev/null and b/src/resources/background/MUME_edit_map.jpg differ diff --git a/src/resources/background/TN-Arrival-in-the-Shire.jpg b/src/resources/background/TN-Arrival-in-the-Shire.jpg new file mode 100644 index 000000000..883683296 Binary files /dev/null and b/src/resources/background/TN-Arrival-in-the-Shire.jpg differ diff --git a/src/resources/background/TN-Arrival_at_Caras_Galadhon.jpg b/src/resources/background/TN-Arrival_at_Caras_Galadhon.jpg new file mode 100644 index 000000000..e36fb4965 Binary files /dev/null and b/src/resources/background/TN-Arrival_at_Caras_Galadhon.jpg differ diff --git a/src/resources/background/TN-Moria-Gate-2021.jpg b/src/resources/background/TN-Moria-Gate-2021.jpg new file mode 100644 index 000000000..5620fc4cb Binary files /dev/null and b/src/resources/background/TN-Moria-Gate-2021.jpg differ diff --git a/src/resources/background/TN-Rivendell-Looking-West.jpg b/src/resources/background/TN-Rivendell-Looking-West.jpg new file mode 100644 index 000000000..66471ea74 Binary files /dev/null and b/src/resources/background/TN-Rivendell-Looking-West.jpg differ diff --git a/src/resources/background/TN-Rivendell.jpg b/src/resources/background/TN-Rivendell.jpg new file mode 100644 index 000000000..20369c158 Binary files /dev/null and b/src/resources/background/TN-Rivendell.jpg differ diff --git a/src/resources/background/background-image.png b/src/resources/background/background-image.png new file mode 100644 index 000000000..80fe83953 Binary files /dev/null and b/src/resources/background/background-image.png differ diff --git a/src/resources/shaders/legacy/plain/ucolor/frag.glsl b/src/resources/shaders/legacy/plain/ucolor/frag.glsl index 6dbf05b1d..09b648dae 100644 --- a/src/resources/shaders/legacy/plain/ucolor/frag.glsl +++ b/src/resources/shaders/legacy/plain/ucolor/frag.glsl @@ -2,8 +2,187 @@ // Copyright (C) 2019 The MMapper Authors uniform vec4 uColor; +uniform vec3 uPlayerPos; // Player position in world space (x, y, z) +uniform int uCurrentLayer; // Current focused layer (player's Z) +uniform int uEnableRadial; // 1 to enable radial transparency, 0 to disable +uniform int uIsNight; // 1 if night time, 0 otherwise + +varying vec3 vWorldPos; + +// 2D noise function (based on value noise) +float noise(vec2 p) +{ + vec2 i = floor(p); + vec2 f = fract(p); + + // Smooth interpolation (smoothstep) + f = f * f * (3.0 - 2.0 * f); + + // Hash function for corners + float a = fract(sin(dot(i, vec2(127.1, 311.7))) * 43758.5453); + float b = fract(sin(dot(i + vec2(1.0, 0.0), vec2(127.1, 311.7))) * 43758.5453); + float c = fract(sin(dot(i + vec2(0.0, 1.0), vec2(127.1, 311.7))) * 43758.5453); + float d = fract(sin(dot(i + vec2(1.0, 1.0), vec2(127.1, 311.7))) * 43758.5453); + + // Bilinear interpolation + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +// Fractal noise (multiple octaves for more detail) +float fractalNoise(vec2 p, int octaves) +{ + float value = 0.0; + float amplitude = 0.5; + float frequency = 1.0; + + for (int i = 0; i < 4; i++) { + if (i >= octaves) break; + value += amplitude * noise(p * frequency); + frequency *= 2.0; + amplitude *= 0.5; + } + + return value; +} + +// Organic pattern for transparent layers (like FastNoise) +float organicPattern(vec2 pos, float scale, float threshold) +{ + // Use fractal noise (2 octaves) for organic, irregular patterns with moderate detail + // This creates smaller transparency patches with sharper edges + float n = fractalNoise(pos * scale, 2); // 2 octaves for balanced organic variation + + // Create holes/gaps based on threshold with sharper transition for defined edges + // Narrower smoothstep creates sharper, more defined transitions + return smoothstep(threshold - 0.12, threshold + 0.12, n); +} void main() { - gl_FragColor = uColor; + vec4 color = uColor; + + // Only apply radial transparency if enabled and NOT on the focused layer + if (uEnableRadial == 1) { + int layerOffset = int(vWorldPos.z) - uCurrentLayer; + int layerDistance = int(abs(float(layerOffset))); + + // Determine if this is an upper or lower layer + bool isUpperLayer = layerOffset > 0; + bool isLowerLayer = layerOffset < 0; + + // Skip masking entirely for elements on the focused layer + if (layerDistance > 0) { + // Calculate grid-aligned distance (Chebyshev distance for room-based grid) + float distX = abs(vWorldPos.x - uPlayerPos.x); + float distY = abs(vWorldPos.y - uPlayerPos.y); + float distBase = max(distX, distY); // Max gives us a square/cubic region aligned to the room grid + + // For lower layers: use uniform opacity based only on layer distance + if (isLowerLayer) { + // Simple layer-based transparency: fade based on how many layers below + float baseAlpha; + + if (layerDistance <= 2) { + // 1-2 layers below: more visible + baseAlpha = 0.65; + } else if (layerDistance <= 5) { + // 3-5 layers below: moderate visibility + baseAlpha = 0.45; + } else if (layerDistance <= 10) { + // 6-10 layers below: low visibility + baseAlpha = 0.25; + } else { + // More than 10 layers below: invisible + baseAlpha = 0.0; + } + + // Apply uniform alpha across the entire layer + color.a = min(color.a, baseAlpha); + } + // For upper layers: apply organic distortion and organic pattern mask + else if (isUpperLayer) { + // Add organic distortion to the distance to make edges irregular and natural + // Use three noise frequencies for highly irregular, jagged edge variation with big waves + float edgeNoise1 = fractalNoise(vWorldPos.xy * 0.4, 2); // Large-scale variation (doubled frequency) + float edgeNoise2 = fractalNoise(vWorldPos.xy * 1.2, 2); // Medium-scale variation (doubled frequency) + float edgeNoise3 = fractalNoise(vWorldPos.xy * 3.0, 3); // High-frequency for very jagged edges (doubled frequency) + // Combine noise layers with doubled amplitude for sharper, bigger waves + float distortion = ((edgeNoise1 - 0.5) * 1.0) + ((edgeNoise2 - 0.5) * 0.7) + ((edgeNoise3 - 0.5) * 0.5); // ±1.1 total range (doubled) + float dist = distBase + distortion; // Apply distortion to create highly organic edges with big waves + + // Graduated radial zone with layer visibility based on distance (reduced radius): + // > 1.3 rooms away: all 10 layers visible (no modification) + // 0.7-1.3 rooms away: 5 layers visible + // 0.4-0.7 rooms away: 2 layers visible + // Close to player (dist < 0.4): only 1 layer visible (includes walls) + + int maxVisibleLayers = 10; // Default: all layers visible + + if (dist < 0.4) { + // Close to player (including walls): only 1 layer + maxVisibleLayers = 1; + } else if (dist < 0.7) { + // 0.4-0.7 rooms away: 2 layers visible + maxVisibleLayers = 2; + } else if (dist < 1.3) { + // 0.7-1.3 rooms away: 5 layers visible + maxVisibleLayers = 5; + } + // else: > 1.3 rooms away, all 10 layers visible + + if (layerDistance > maxVisibleLayers) { + // Hide layers beyond the visible limit for this distance + color.a = 0.0; + } else if (maxVisibleLayers < 10) { + // Determine base alpha based on layer distance with new gradual fade + float baseAlpha; + + if (layerDistance == 1) { + baseAlpha = 0.7; + } else if (layerDistance == 2) { + baseAlpha = 0.6; + } else if (layerDistance == 3) { + baseAlpha = 0.5; + } else if (layerDistance == 4) { + baseAlpha = 0.4; + } else if (layerDistance == 5) { + baseAlpha = 0.3; + } else if (layerDistance == 6) { + baseAlpha = 0.2; + } else if (layerDistance == 7) { + baseAlpha = 0.15; + } else if (layerDistance == 8) { + baseAlpha = 0.1; + } else if (layerDistance == 9) { + baseAlpha = 0.05; + } else { + // 10+ layers above: invisible + baseAlpha = 0.0; + } + + // Apply organic noise pattern to visible transparent layers near player + // Higher scale for smaller, more defined organic patches + float scale = 0.13; // Smaller organic patches with sharper edges + float threshold = 0.48; // Adjusted for better balance of visible/transparent areas + float pattern = organicPattern(vWorldPos.xy, scale, threshold); + // Convert gradient to binary mask for clean transparency + // Below 0.5: fully transparent (alpha = 0), Above 0.5: use baseAlpha + float holeAlpha = pattern < 0.5 ? 0.0 : 1.0; + + // Apply pattern to base alpha + // Where pattern is low (holes), alpha goes to 0 (fully transparent) + // Where pattern is high, use base alpha + color.a = min(color.a, baseAlpha * holeAlpha); + } + // else: Far from player (maxVisibleLayers == 10), keep original alpha + } + } + } + + // Apply night darkening to the current layer (20-30% darker) + if (uIsNight == 1 && int(vWorldPos.z) == uCurrentLayer) { + color.rgb *= 0.75; // 25% darker at night + } + + gl_FragColor = color; } diff --git a/src/resources/shaders/legacy/plain/ucolor/vert.glsl b/src/resources/shaders/legacy/plain/ucolor/vert.glsl index 8ee8eab1b..f88a9e3f4 100644 --- a/src/resources/shaders/legacy/plain/ucolor/vert.glsl +++ b/src/resources/shaders/legacy/plain/ucolor/vert.glsl @@ -5,7 +5,10 @@ uniform mat4 uMVP; attribute vec3 aVert; +varying vec3 vWorldPos; + void main() { + vWorldPos = aVert; gl_Position = uMVP * vec4(aVert, 1.0); } diff --git a/src/resources/shaders/legacy/tex/acolor/frag.glsl b/src/resources/shaders/legacy/tex/acolor/frag.glsl index 39d58ba8d..a9c107803 100644 --- a/src/resources/shaders/legacy/tex/acolor/frag.glsl +++ b/src/resources/shaders/legacy/tex/acolor/frag.glsl @@ -3,11 +3,190 @@ uniform sampler2D uTexture; uniform vec4 uColor; +uniform vec3 uPlayerPos; // Player position in world space (x, y, z) +uniform int uCurrentLayer; // Current focused layer (player's Z) +uniform int uEnableRadial; // 1 to enable radial transparency, 0 to disable +uniform int uTexturesDisabled; // 1 if textures are disabled (non-textured mode), 0 otherwise +uniform int uIsNight; // 1 if night time, 0 otherwise varying vec4 vColor; varying vec2 vTexCoord; +varying vec3 vWorldPos; + +// 2D noise function (based on value noise) +float noise(vec2 p) +{ + vec2 i = floor(p); + vec2 f = fract(p); + + // Smooth interpolation (smoothstep) + f = f * f * (3.0 - 2.0 * f); + + // Hash function for corners + float a = fract(sin(dot(i, vec2(127.1, 311.7))) * 43758.5453); + float b = fract(sin(dot(i + vec2(1.0, 0.0), vec2(127.1, 311.7))) * 43758.5453); + float c = fract(sin(dot(i + vec2(0.0, 1.0), vec2(127.1, 311.7))) * 43758.5453); + float d = fract(sin(dot(i + vec2(1.0, 1.0), vec2(127.1, 311.7))) * 43758.5453); + + // Bilinear interpolation + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +// Fractal noise (multiple octaves for more detail) +float fractalNoise(vec2 p, int octaves) +{ + float value = 0.0; + float amplitude = 0.5; + float frequency = 1.0; + + for (int i = 0; i < 4; i++) { + if (i >= octaves) break; + value += amplitude * noise(p * frequency); + frequency *= 2.0; + amplitude *= 0.5; + } + + return value; +} + +// Organic pattern for transparent layers (like FastNoise) +float organicPattern(vec2 pos, float scale, float threshold) +{ + // Use fractal noise (2 octaves) for organic, irregular patterns with moderate detail + // This creates smaller transparency patches with sharper edges + float n = fractalNoise(pos * scale, 2); // 2 octaves for balanced organic variation + + // Create holes/gaps based on threshold with sharper transition for defined edges + // Narrower smoothstep creates sharper, more defined transitions + return smoothstep(threshold - 0.12, threshold + 0.12, n); +} void main() { - gl_FragColor = vColor * uColor * texture2D(uTexture, vTexCoord); + vec4 texColor = vColor * uColor * texture2D(uTexture, vTexCoord); + + // Only apply radial transparency if enabled and NOT on the focused layer + if (uEnableRadial == 1) { + int layerOffset = int(vWorldPos.z) - uCurrentLayer; + int layerDistance = int(abs(float(layerOffset))); + + // Determine if this is an upper or lower layer + bool isUpperLayer = layerOffset > 0; + bool isLowerLayer = layerOffset < 0; + + // Skip masking entirely for elements on the focused layer + if (layerDistance > 0) { + // Calculate grid-aligned distance (Chebyshev distance for room-based grid) + float distX = abs(vWorldPos.x - uPlayerPos.x); + float distY = abs(vWorldPos.y - uPlayerPos.y); + float distBase = max(distX, distY); // Max gives us a square/cubic region aligned to the room grid + + // For lower layers: use uniform opacity based only on layer distance + if (isLowerLayer) { + // Simple layer-based transparency: fade based on how many layers below + float baseAlpha; + + if (layerDistance <= 2) { + // 1-2 layers below: more visible + baseAlpha = 0.65; + } else if (layerDistance <= 5) { + // 3-5 layers below: moderate visibility + baseAlpha = 0.45; + } else if (layerDistance <= 10) { + // 6-10 layers below: low visibility + baseAlpha = 0.25; + } else { + // More than 10 layers below: invisible + baseAlpha = 0.0; + } + + // Apply uniform alpha across the entire layer + texColor.a = min(texColor.a, baseAlpha); + } + // For upper layers: apply organic distortion and organic pattern mask + else if (isUpperLayer) { + // Add organic distortion to the distance to make edges irregular and natural + // Use three noise frequencies for highly irregular, jagged edge variation with big waves + float edgeNoise1 = fractalNoise(vWorldPos.xy * 0.4, 2); // Large-scale variation (doubled frequency) + float edgeNoise2 = fractalNoise(vWorldPos.xy * 1.2, 2); // Medium-scale variation (doubled frequency) + float edgeNoise3 = fractalNoise(vWorldPos.xy * 3.0, 3); // High-frequency for very jagged edges (doubled frequency) + // Combine noise layers with doubled amplitude for sharper, bigger waves + float distortion = ((edgeNoise1 - 0.5) * 1.0) + ((edgeNoise2 - 0.5) * 0.7) + ((edgeNoise3 - 0.5) * 0.5); // ±1.1 total range (doubled) + float dist = distBase + distortion; // Apply distortion to create highly organic edges with big waves + + // Graduated radial zone with layer visibility based on distance (reduced radius): + // > 1.3 rooms away: all 10 layers visible (no modification) + // 0.7-1.3 rooms away: 5 layers visible + // 0.4-0.7 rooms away: 2 layers visible + // Close to player (dist < 0.4): only 1 layer visible (includes walls) + + int maxVisibleLayers = 10; // Default: all layers visible + + if (dist < 0.4) { + // Close to player (including walls): only 1 layer + maxVisibleLayers = 1; + } else if (dist < 0.7) { + // 0.4-0.7 rooms away: 2 layers visible + maxVisibleLayers = 2; + } else if (dist < 1.3) { + // 0.7-1.3 rooms away: 5 layers visible + maxVisibleLayers = 5; + } + // else: > 1.3 rooms away, all 10 layers visible + + if (layerDistance > maxVisibleLayers) { + // Hide layers beyond the visible limit for this distance + texColor.a = 0.0; + } else if (maxVisibleLayers < 10) { + // Determine base alpha based on layer distance with new gradual fade + float baseAlpha; + + if (layerDistance == 1) { + baseAlpha = 0.5; + } else if (layerDistance == 2) { + baseAlpha = 0.4; + } else if (layerDistance == 3) { + baseAlpha = 0.3; + } else if (layerDistance == 4) { + baseAlpha = 0.2; + } else if (layerDistance == 5) { + baseAlpha = 0.1; + } else if (layerDistance == 6) { + baseAlpha = 0.08; + } else if (layerDistance == 7) { + baseAlpha = 0.06; + } else if (layerDistance == 8) { + baseAlpha = 0.04; + } else if (layerDistance == 9) { + baseAlpha = 0.02; + } else { + // 10+ layers above: invisible + baseAlpha = 0.0; + } + + // Apply organic noise pattern to visible transparent layers near player + // Higher scale for smaller, more defined organic patches + float scale = 99.9; // Smaller organic patches with sharper edges + float threshold = 1.1; // Adjusted for better balance of visible/transparent areas + float pattern = organicPattern(vWorldPos.xy, scale, threshold); + // Convert gradient to binary mask for clean transparency + // Below 0.5: fully transparent (alpha = 0), Above 0.5: use baseAlpha + float holeAlpha = pattern < 0.5 ? 0.0 : 1.0; + + // Apply pattern to base alpha + // Where pattern is low (holes), alpha goes to 0 (fully transparent) + // Where pattern is high, use base alpha + texColor.a = min(texColor.a, baseAlpha * holeAlpha); + } + // else: Far from player (maxVisibleLayers == 10), keep original alpha + } + } + } + + // Apply night darkening to the current layer (20-30% darker) + if (uIsNight == 1 && int(vWorldPos.z) == uCurrentLayer) { + texColor.rgb *= 0.75; // 25% darker at night + } + + gl_FragColor = texColor; } diff --git a/src/resources/shaders/legacy/tex/acolor/vert.glsl b/src/resources/shaders/legacy/tex/acolor/vert.glsl index 371657c84..432ec9fef 100644 --- a/src/resources/shaders/legacy/tex/acolor/vert.glsl +++ b/src/resources/shaders/legacy/tex/acolor/vert.glsl @@ -9,10 +9,12 @@ attribute vec3 aVert; varying vec4 vColor; varying vec2 vTexCoord; +varying vec3 vWorldPos; void main() { vColor = aColor; vTexCoord = aTexCoord; + vWorldPos = aVert; gl_Position = uMVP * vec4(aVert, 1.0); } diff --git a/src/resources/shaders/legacy/tex/ucolor/frag.glsl b/src/resources/shaders/legacy/tex/ucolor/frag.glsl index c62986469..dfbcb3d54 100644 --- a/src/resources/shaders/legacy/tex/ucolor/frag.glsl +++ b/src/resources/shaders/legacy/tex/ucolor/frag.glsl @@ -3,10 +3,188 @@ uniform sampler2D uTexture; uniform vec4 uColor; +uniform vec3 uPlayerPos; // Player position in world space (x, y, z) +uniform int uCurrentLayer; // Current focused layer (player's Z) +uniform int uEnableRadial; // 1 to enable radial transparency, 0 to disable +uniform int uIsNight; // 1 if night time, 0 otherwise varying vec2 vTexCoord; +varying vec3 vWorldPos; + +// 2D noise function (based on value noise) +float noise(vec2 p) +{ + vec2 i = floor(p); + vec2 f = fract(p); + + // Smooth interpolation (smoothstep) + f = f * f * (3.0 - 2.0 * f); + + // Hash function for corners + float a = fract(sin(dot(i, vec2(127.1, 311.7))) * 43758.5453); + float b = fract(sin(dot(i + vec2(1.0, 0.0), vec2(127.1, 311.7))) * 43758.5453); + float c = fract(sin(dot(i + vec2(0.0, 1.0), vec2(127.1, 311.7))) * 43758.5453); + float d = fract(sin(dot(i + vec2(1.0, 1.0), vec2(127.1, 311.7))) * 43758.5453); + + // Bilinear interpolation + return mix(mix(a, b, f.x), mix(c, d, f.x), f.y); +} + +// Fractal noise (multiple octaves for more detail) +float fractalNoise(vec2 p, int octaves) +{ + float value = 0.0; + float amplitude = 0.5; + float frequency = 1.0; + + for (int i = 0; i < 4; i++) { + if (i >= octaves) break; + value += amplitude * noise(p * frequency); + frequency *= 2.0; + amplitude *= 0.5; + } + + return value; +} + +// Organic pattern for transparent layers (like FastNoise) +float organicPattern(vec2 pos, float scale, float threshold) +{ + // Use fractal noise (2 octaves) for organic, irregular patterns with moderate detail + // This creates smaller transparency patches with sharper edges + float n = fractalNoise(pos * scale, 2); // 2 octaves for balanced organic variation + + // Create holes/gaps based on threshold with sharper transition for defined edges + // Narrower smoothstep creates sharper, more defined transitions + return smoothstep(threshold - 0.12, threshold + 0.12, n); +} void main() { - gl_FragColor = uColor * texture2D(uTexture, vTexCoord); + vec4 texColor = uColor * texture2D(uTexture, vTexCoord); + + // Only apply radial transparency if enabled and NOT on the focused layer + if (uEnableRadial == 1) { + int layerOffset = int(vWorldPos.z) - uCurrentLayer; + int layerDistance = int(abs(float(layerOffset))); + + // Determine if this is an upper or lower layer + bool isUpperLayer = layerOffset > 0; + bool isLowerLayer = layerOffset < 0; + + // Skip masking entirely for elements on the focused layer + if (layerDistance > 0) { + // Calculate grid-aligned distance (Chebyshev distance for room-based grid) + float distX = abs(vWorldPos.x - uPlayerPos.x); + float distY = abs(vWorldPos.y - uPlayerPos.y); + float distBase = max(distX, distY); // Max gives us a square/cubic region aligned to the room grid + + // For lower layers: use uniform opacity based only on layer distance + if (isLowerLayer) { + // Simple layer-based transparency: fade based on how many layers below + float baseAlpha; + + if (layerDistance <= 2) { + // 1-2 layers below: more visible + baseAlpha = 0.65; + } else if (layerDistance <= 5) { + // 3-5 layers below: moderate visibility + baseAlpha = 0.45; + } else if (layerDistance <= 10) { + // 6-10 layers below: low visibility + baseAlpha = 0.25; + } else { + // More than 10 layers below: invisible + baseAlpha = 0.0; + } + + // Apply uniform alpha across the entire layer + texColor.a = min(texColor.a, baseAlpha); + } + // For upper layers: apply organic distortion and organic pattern mask + else if (isUpperLayer) { + // Add organic distortion to the distance to make edges irregular and natural + // Use three noise frequencies for highly irregular, jagged edge variation with big waves + float edgeNoise1 = fractalNoise(vWorldPos.xy * 0.4, 2); // Large-scale variation (doubled frequency) + float edgeNoise2 = fractalNoise(vWorldPos.xy * 1.2, 2); // Medium-scale variation (doubled frequency) + float edgeNoise3 = fractalNoise(vWorldPos.xy * 3.0, 3); // High-frequency for very jagged edges (doubled frequency) + // Combine noise layers with doubled amplitude for sharper, bigger waves + float distortion = ((edgeNoise1 - 0.5) * 1.0) + ((edgeNoise2 - 0.5) * 0.7) + ((edgeNoise3 - 0.5) * 0.5); // ±1.1 total range (doubled) + float dist = distBase + distortion; // Apply distortion to create highly organic edges with big waves + + // Graduated radial zone with layer visibility based on distance (reduced radius): + // > 1.3 rooms away: all 10 layers visible (no modification) + // 0.7-1.3 rooms away: 5 layers visible + // 0.4-0.7 rooms away: 2 layers visible + // Close to player (dist < 0.4): only 1 layer visible (includes walls) + + int maxVisibleLayers = 10; // Default: all layers visible + + if (dist < 0.4) { + // Close to player (including walls): only 1 layer + maxVisibleLayers = 1; + } else if (dist < 0.7) { + // 0.4-0.7 rooms away: 2 layers visible + maxVisibleLayers = 2; + } else if (dist < 1.3) { + // 0.7-1.3 rooms away: 5 layers visible + maxVisibleLayers = 5; + } + // else: > 1.3 rooms away, all 10 layers visible + + if (layerDistance > maxVisibleLayers) { + // Hide layers beyond the visible limit for this distance + texColor.a = 0.0; + } else if (maxVisibleLayers < 10) { + // Determine base alpha based on layer distance with new gradual fade + float baseAlpha; + + if (layerDistance == 1) { + baseAlpha = 0.5; + } else if (layerDistance == 2) { + baseAlpha = 0.4; + } else if (layerDistance == 3) { + baseAlpha = 0.3; + } else if (layerDistance == 4) { + baseAlpha = 0.2; + } else if (layerDistance == 5) { + baseAlpha = 0.1; + } else if (layerDistance == 6) { + baseAlpha = 0.08; + } else if (layerDistance == 7) { + baseAlpha = 0.06; + } else if (layerDistance == 8) { + baseAlpha = 0.04; + } else if (layerDistance == 9) { + baseAlpha = 0.02; + } else { + // 10+ layers above: invisible + baseAlpha = 0.0; + } + + // Apply organic noise pattern to visible transparent layers near player + // Higher scale for smaller, more defined organic patches + float scale = 99.9; // Smaller organic patches with sharper edges + float threshold = 1.1; // Adjusted for better balance of visible/transparent areas + float pattern = organicPattern(vWorldPos.xy, scale, threshold); + // Convert gradient to binary mask for clean transparency + // Below 0.5: fully transparent (alpha = 0), Above 0.5: use baseAlpha + float holeAlpha = pattern < 0.5 ? 0.0 : 1.0; + + // Apply pattern to base alpha + // Where pattern is low (holes), alpha goes to 0 (fully transparent) + // Where pattern is high, use base alpha + texColor.a = min(texColor.a, baseAlpha * holeAlpha); + } + // else: Far from player (maxVisibleLayers == 10), keep original alpha + } + } + } + + // Apply night darkening to the current layer (20-30% darker) + if (uIsNight == 1 && int(vWorldPos.z) == uCurrentLayer) { + texColor.rgb *= 0.75; // 25% darker at night + } + + gl_FragColor = texColor; } diff --git a/src/resources/shaders/legacy/tex/ucolor/vert.glsl b/src/resources/shaders/legacy/tex/ucolor/vert.glsl index ace5c1793..415a0ddc6 100644 --- a/src/resources/shaders/legacy/tex/ucolor/vert.glsl +++ b/src/resources/shaders/legacy/tex/ucolor/vert.glsl @@ -7,9 +7,11 @@ attribute vec2 aTexCoord; attribute vec3 aVert; varying vec2 vTexCoord; +varying vec3 vWorldPos; void main() { vTexCoord = aTexCoord; + vWorldPos = aVert; gl_Position = uMVP * vec4(aVert, 1.0); }