diff --git a/ImguiMapEditor/Application/CallbackMediator.cpp b/ImguiMapEditor/Application/CallbackMediator.cpp index 2420711..7eca4c7 100644 --- a/ImguiMapEditor/Application/CallbackMediator.cpp +++ b/ImguiMapEditor/Application/CallbackMediator.cpp @@ -213,6 +213,8 @@ void CallbackMediator::wireTabCallbacks(Context &ctx) { state.current_floor = ctx.map_panel->getCurrentFloor(); state.lighting_enabled = ctx.view_settings->map_lighting_enabled; state.ambient_light = ctx.view_settings->map_ambient_light; + state.server_light_intensity = ctx.view_settings->server_light_intensity; + state.server_light_color = ctx.view_settings->server_light_color; state.show_ingame_box = ctx.view_settings->show_ingame_box; state.show_minimap = ctx.view_settings->show_minimap_window; @@ -238,6 +240,8 @@ void CallbackMediator::wireTabCallbacks(Context &ctx) { ctx.map_panel->setCurrentFloor(static_cast(state.current_floor)); ctx.view_settings->map_lighting_enabled = state.lighting_enabled; ctx.view_settings->map_ambient_light = state.ambient_light; + ctx.view_settings->server_light_intensity = state.server_light_intensity; + ctx.view_settings->server_light_color = state.server_light_color; ctx.view_settings->show_ingame_box = state.show_ingame_box; ctx.view_settings->show_minimap_window = state.show_minimap; ctx.minimap->setMap(session->getMap(), diff --git a/ImguiMapEditor/Application/EditorSession.h b/ImguiMapEditor/Application/EditorSession.h index 58de8ec..fcd8954 100644 --- a/ImguiMapEditor/Application/EditorSession.h +++ b/ImguiMapEditor/Application/EditorSession.h @@ -118,7 +118,9 @@ class EditorSession { int current_floor = 7; // Per-map lighting settings bool lighting_enabled = false; - int ambient_light = 128; + int ambient_light = 0; + uint8_t server_light_intensity = 200; + uint8_t server_light_color = 215; // Per-map window visibility bool show_ingame_box = false; bool show_minimap = false; diff --git a/ImguiMapEditor/CMakeLists.txt b/ImguiMapEditor/CMakeLists.txt index b3ea327..ba4e427 100644 --- a/ImguiMapEditor/CMakeLists.txt +++ b/ImguiMapEditor/CMakeLists.txt @@ -295,6 +295,7 @@ set(UI_SOURCES UI/Ribbon/RibbonController.cpp UI/Widgets/Properties/PropertyPanelRenderer.cpp UI/Widgets/Properties/PropertyWidgets.cpp + UI/Widgets/LightColorPalettePicker.cpp UI/Widgets/QuickSearchPopup.cpp UI/Widgets/SearchResultsWidget.cpp UI/Widgets/TilesetWidget.cpp diff --git a/ImguiMapEditor/Core/Config.h b/ImguiMapEditor/Core/Config.h index 9cc67af..4ae18f0 100644 --- a/ImguiMapEditor/Core/Config.h +++ b/ImguiMapEditor/Core/Config.h @@ -83,6 +83,19 @@ inline constexpr int GROUND_LAYER = 7; inline constexpr uint16_t DEFAULT_MAP_SIZE = 16384; } // namespace Map +// ============================================================================ +// LIGHTING CONFIGURATION +// ============================================================================ +namespace Lighting { +inline constexpr int LIGHT_COLOR_COUNT = 216; +inline constexpr uint8_t MAX_SERVER_LIGHT_COLOR = 215; +inline constexpr uint8_t DEFAULT_SERVER_LIGHT_INTENSITY = 200; +inline constexpr uint8_t DEFAULT_SERVER_LIGHT_COLOR = 215; +inline constexpr uint8_t DEFAULT_MINIMUM_AMBIENT = 0; +inline constexpr uint8_t DEFAULT_PLAYER_LIGHT_INTENSITY = 2; +inline constexpr uint8_t TRANSLUCENT_PROPAGATION_INTENSITY = 1; +} // namespace Lighting + // ============================================================================ // PERFORMANCE CONFIGURATION // ============================================================================ diff --git a/ImguiMapEditor/Domain/CreatureType.h b/ImguiMapEditor/Domain/CreatureType.h index e272e9b..b5e0f25 100644 --- a/ImguiMapEditor/Domain/CreatureType.h +++ b/ImguiMapEditor/Domain/CreatureType.h @@ -9,6 +9,10 @@ struct CreatureType { std::string name; bool is_npc = false; Outfit outfit; + + // Light properties (from DAT or creatures.xml) + uint8_t light_level = 0; // 0 = no light, >0 = light radius + uint8_t light_color = 0; // 8-bit Tibia palette index // Default constructor CreatureType() = default; diff --git a/ImguiMapEditor/Domain/ItemType.h b/ImguiMapEditor/Domain/ItemType.h index b9b4cfb..d62addf 100644 --- a/ImguiMapEditor/Domain/ItemType.h +++ b/ImguiMapEditor/Domain/ItemType.h @@ -227,6 +227,9 @@ class ItemType { // Translucency (from DAT) bool is_translucent = false; + // Lens help (from DAT) — items with value > 0 are "see-through" for light propagation + uint16_t lens_help = 0; + // Elevation (from DAT) - items on this raise subsequent items visually uint16_t elevation = 0; diff --git a/ImguiMapEditor/Domain/LightTypes.h b/ImguiMapEditor/Domain/LightTypes.h index 98c547c..e204d05 100644 --- a/ImguiMapEditor/Domain/LightTypes.h +++ b/ImguiMapEditor/Domain/LightTypes.h @@ -1,10 +1,21 @@ #pragma once +#include "Core/Config.h" + #include namespace MapEditor { namespace Domain { +/** + * Global world light (mimics server WorldLight protocol message). + * Defines the ambient light level for above-ground floors. + */ +struct GlobalLight { + uint8_t intensity = 0; // Server ambient level (0-255) + uint8_t color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; +}; + /** * A light source extracted from a tile item. * Contains position, color (8-bit palette index), and intensity. @@ -12,8 +23,9 @@ namespace Domain { struct LightSource { int32_t x = 0; // Tile X position int32_t y = 0; // Tile Y position - uint8_t color = 215; // 8-bit palette index (default: white-ish) + uint8_t color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; uint8_t intensity = 0; // 0-255 (affects light radius) + int16_t source_floor = 0; // Floor this light originates from (for blocking) }; /** @@ -22,8 +34,23 @@ struct LightSource { */ struct LightConfig { bool enabled = false; // Master enable for this viewport - uint8_t ambient_level = 255; // 0 = complete darkness, 255 = full bright - uint8_t ambient_color = 215; // 8-bit palette index for ambient + GlobalLight global_light; // Server global light (above-ground ambient) + uint8_t client_slider = Config::Lighting::DEFAULT_MINIMUM_AMBIENT; + int16_t camera_floor = Config::Map::GROUND_LAYER; + // Deprecated compatibility fields. Rendering computes final ambient from + // global_light, client_slider, and camera_floor elsewhere. + uint8_t ambient_color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; + uint8_t ambient_level = 255; + + /** + * Hash fields that affect generated lighting. + */ + [[nodiscard]] uint32_t computeHash() const { + return static_cast(global_light.intensity) | + (static_cast(global_light.color) << 8) | + (static_cast(client_slider) << 16) | + (static_cast(camera_floor & 0xFF) << 24); + } }; } // namespace Domain diff --git a/ImguiMapEditor/Domain/Tile.h b/ImguiMapEditor/Domain/Tile.h index 5dd51cc..f367b6e 100644 --- a/ImguiMapEditor/Domain/Tile.h +++ b/ImguiMapEditor/Domain/Tile.h @@ -26,7 +26,10 @@ enum class TileFlag : uint16_t { NoPvp = 1 << 2, // 0x0004 NoLogout = 1 << 3, // 0x0008 PvpZone = 1 << 4, // 0x0010 - Refresh = 1 << 5 // 0x0020 + Refresh = 1 << 5, // 0x0020 + // TranslucentLight marks an underground tile lit by translucent ground above; + // the current RME/OTClient propagation rule applies at z == GROUND_LAYER + 1. + TranslucentLight = 1 << 6 }; inline TileFlag operator|(TileFlag a, TileFlag b) { diff --git a/ImguiMapEditor/IO/CreatureXmlReader.cpp b/ImguiMapEditor/IO/CreatureXmlReader.cpp index 8390e98..8d03d64 100644 --- a/ImguiMapEditor/IO/CreatureXmlReader.cpp +++ b/ImguiMapEditor/IO/CreatureXmlReader.cpp @@ -135,6 +135,25 @@ CreatureXmlReader::parseCreatureNode(const pugi::xml_node &node, bool isNpc, creature->outfit.lookMountFeet = static_cast(attr.as_uint()); } + // Light properties (optional) + const auto read_light_u8 = [&](const char *attribute_name) -> uint8_t { + const auto attr = node.attribute(attribute_name); + if (!attr) return 0; + + const unsigned int raw_value = attr.as_uint(); + if (raw_value > 255U) { + warnings.push_back(std::string("Creature '") + creature->name + + "' has " + attribute_name + "=" + + std::to_string(raw_value) + "; clamped to 255"); + return 255; + } + + return static_cast(raw_value); + }; + + creature->light_level = read_light_u8("lightlevel"); + creature->light_color = read_light_u8("lightcolor"); + return creature; } diff --git a/ImguiMapEditor/Presentation/MainWindow.cpp b/ImguiMapEditor/Presentation/MainWindow.cpp index ab63ebf..fa94ace 100644 --- a/ImguiMapEditor/Presentation/MainWindow.cpp +++ b/ImguiMapEditor/Presentation/MainWindow.cpp @@ -2,6 +2,7 @@ #include "Rendering/Frame/RenderingManager.h" #include "Services/ClipboardService.h" #include "UI/Core/Theme.h" +#include "UI/Widgets/LightColorPalettePicker.h" #include #include #include @@ -134,10 +135,28 @@ void MainWindow::renderEditor(Domain::ChunkedMap *current_map, // Lighting controls toolbar (per-map) ImGui::Checkbox("Enable Lighting", &view_settings_.map_lighting_enabled); - ImGui::SameLine(); - ImGui::SetNextItemWidth(120); - ImGui::SliderInt("Ambient", &view_settings_.map_ambient_light, 0, - 255); + if (view_settings_.map_lighting_enabled) { + ImGui::SameLine(); + ImGui::SetNextItemWidth(120); + int intensity = static_cast(view_settings_.server_light_intensity); + if (ImGui::SliderInt("Intensity", &intensity, 0, 255)) { + view_settings_.server_light_intensity = static_cast(intensity); + } + ImGui::SameLine(); + ImGui::TextUnformatted("Color"); + ImGui::SameLine(); + uint8_t color = view_settings_.server_light_color; + if (UI::LightColorPalettePicker( + "##serverLightColor", + color, + true, + "Server Color: world light color from the Tibia 8-bit palette")) { + view_settings_.server_light_color = color; + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(120); + ImGui::SliderInt("Min Ambient", &view_settings_.map_ambient_light, 0, 255); + } ImGui::Separator(); // Render the map panel with explicit animation ticks diff --git a/ImguiMapEditor/Rendering/Core/IRenderPass.h b/ImguiMapEditor/Rendering/Core/IRenderPass.h index d7247f5..190ab3f 100644 --- a/ImguiMapEditor/Rendering/Core/IRenderPass.h +++ b/ImguiMapEditor/Rendering/Core/IRenderPass.h @@ -1,11 +1,13 @@ #pragma once #include "Domain/ChunkedMap.h" +#include "Domain/Position.h" #include "Rendering/Animation/AnimationTicks.h" #include "Rendering/Camera/ViewCamera.h" #include "Rendering/Frame/RenderState.h" #include "Rendering/Visibility/VisibleBounds.h" #include +#include namespace MapEditor { namespace Services { @@ -44,6 +46,9 @@ struct RenderContext { // View settings (optional, can be nullptr) const Services::ViewSettings *view_settings; + + // RME light-mode floor visibility origin, set from the last map click. + std::optional light_visibility_origin; }; /** diff --git a/ImguiMapEditor/Rendering/Frame/RenderState.h b/ImguiMapEditor/Rendering/Frame/RenderState.h index e44d318..2602f02 100644 --- a/ImguiMapEditor/Rendering/Frame/RenderState.h +++ b/ImguiMapEditor/Rendering/Frame/RenderState.h @@ -45,6 +45,7 @@ class RenderState { // === State tracking for cache invalidation === float last_zoom = 0.0f; uint8_t last_ambient_light = 255; + uint32_t last_config_hash = 0; // Hash of LightConfig fields affecting lighting // === Invalidation methods === diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp index 9368789..71a9fdb 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.cpp +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.cpp @@ -1,190 +1,241 @@ #include "LightGatherer.h" + +#include + +#include "Core/Config.h" +#include "Rendering/Light/LightProjection.h" #include "Services/ClientDataService.h" #include "Domain/Tile.h" #include "Domain/Item.h" #include "Domain/ItemType.h" -#include +#include "Domain/Creature.h" +#include "Domain/CreatureType.h" namespace MapEditor { namespace Rendering { -void LightGatherer::clear() { - lights_.clear(); +namespace { + +constexpr int LIGHT_COLLECTION_MARGIN_TILES = 16; + +bool blocksLightFromBelow(const Domain::ItemType* item_type) { + if (!item_type) return false; + const bool is_ground_tile = + item_type->is_ground || item_type->group == Domain::ItemGroup::Ground; + return is_ground_tile && !item_type->is_translucent; } -void LightGatherer::gatherForChunk( - const Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, - Services::ClientDataService* client_data, - int16_t floor) -{ - if (!client_data) return; - - // We need to check the target chunk AND its 8 neighbors (3x3 grid) - // because a light in a neighbor might spill into this chunk. - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - // Get visible chunks directly from the map using exact chunk coordinates - // Note: ChunkedMap doesn't expose "getChunk(cx, cy)" publicly in the interface I saw earlier, - // but it has `getVisibleChunks` which takes pixel/tile coords. - // Let's use tile coordinates to find the chunks. - - int32_t target_cx = chunk_x + dx; - int32_t target_cy = chunk_y + dy; - - // Calculate world tile coordinates for this chunk - int32_t tile_start_x = target_cx * Domain::Chunk::SIZE; - int32_t tile_start_y = target_cy * Domain::Chunk::SIZE; - int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; - int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; - - // Efficiently get the single chunk for this region (or just iterate tiles if API limits) - // The existing `getVisibleChunks` returns a vector. Let's use that. - std::vector chunks; - map.getVisibleChunks(tile_start_x, tile_start_y, tile_end_x - 1, tile_end_y - 1, floor, chunks); - - for (Domain::Chunk* chunk : chunks) { - // Ensure we are processing the correct chunk (getVisibleChunks might return overlaps if not careful, - // but with exact coordinates it should be 1 chunk) - if (!chunk) continue; - - chunk->forEachTile([&](const Domain::Tile* tile) { - if (!tile || tile->getZ() != floor) return; - - int32_t tile_x = tile->getX(); - int32_t tile_y = tile->getY(); - - // Check ground item for light - const Domain::Item* ground = tile->getGround(); - if (ground) { - const Domain::ItemType* item_type = client_data->getItemTypeByServerId(ground->getServerId()); - if (item_type && item_type->light_level > 0) { - Domain::LightSource light; - light.x = tile_x; - light.y = tile_y; - light.color = item_type->light_color; - light.intensity = item_type->light_level; - lights_.push_back(light); - - // Check for invalid color - if (light.color == 0) { - // spdlog::warn("Light source with NO COLOR (Index 0) found at {},{}: ItemID {}, Level {}", tile_x, tile_y, ground->getServerId(), light.intensity); - } else { - // spdlog::debug("Light found at {},{}: ItemID {}, Level {}, Color {}", tile_x, tile_y, ground->getServerId(), light.intensity, light.color); - } - } - } - - // Check all items on the tile - for (const auto& item_ptr : tile->getItems()) { - if (!item_ptr) continue; - const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item_ptr->getServerId()); - if (item_type && item_type->light_level > 0) { - Domain::LightSource light; - light.x = tile_x; - light.y = tile_y; - light.color = item_type->light_color; - light.intensity = item_type->light_level; - lights_.push_back(light); - - if (light.color == 0) { - // spdlog::warn("Light source with NO COLOR (Index 0) found at {},{}: ItemID {}, Level {}", tile_x, tile_y, item_ptr->getServerId(), light.intensity); - } else { - // spdlog::debug("Light found at {},{}: ItemID {}, Level {}, Color {}", tile_x, tile_y, item_ptr->getServerId(), light.intensity, light.color); - } - } - } - }); - } +bool carriesTranslucentLight(const Domain::Tile* tile, + Services::ClientDataService* client_data) { + if (!tile || !client_data) return false; + + const auto check_item = [&](const Domain::Item* item) { + if (!item) return false; + const Domain::ItemType* item_type = item->getType(); + if (!item_type) { + item_type = client_data->getItemTypeByServerId(item->getServerId()); + } + return item_type && + (item_type->is_translucent || item_type->lens_help > 0); + }; + + if (check_item(tile->getGround())) return true; + + for (const auto& item : tile->getItems()) { + if (check_item(item.get())) return true; + } + + return false; +} + +} // namespace + +void LightGatherer::addLight(ViewportLightBuffer& light_buffer, + int32_t x, int32_t y, + uint8_t color, uint8_t intensity, + int16_t floor) { + if (intensity == 0) return; + + if (!light_buffer.lights.empty()) { + auto& prev = light_buffer.lights.back(); + if (prev.x == x && prev.y == y && prev.color == color && prev.source_floor == floor) { + prev.intensity = std::max(prev.intensity, intensity); + return; } } + + light_buffer.lights.emplace_back(Domain::LightSource{ + .x = x, + .y = y, + .color = color, + .intensity = intensity, + .source_floor = floor + }); } -void LightGatherer::gatherForChunkMultiFloor( +void LightGatherer::gatherViewportLightBuffer( const Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, Services::ClientDataService* client_data, + int16_t current_floor, int16_t start_floor, - int16_t end_floor) + int16_t end_floor, + ViewportLightBuffer& light_buffer) { if (!client_data) return; - - constexpr int GROUND_LAYER = 7; - - // Iterate through all floors in range (from start_floor down to end_floor) + + // RME processes start_z down to superend_z. The floor_light_start snapshot + // must be taken before collecting each floor's lights, in that same order. for (int16_t floor = start_floor; floor >= end_floor; --floor) { - - // Calculate isometric offset for this floor (RME-style) - // Lights from higher floors (lower Z) appear shifted in X and Y - int32_t floor_offset = 0; - if (floor <= GROUND_LAYER) { - // Above ground: offset based on distance from ground layer - floor_offset = GROUND_LAYER - floor; + const uint32_t floor_light_start = + static_cast(light_buffer.lights.size()); + + if (floor >= current_floor) { + registerGroundOcclusionForViewport( + map, client_data, current_floor, floor, floor_light_start, light_buffer); } - // Underground floors don't get offset in RME's light system - - // We need to check the target chunk AND its 8 neighbors (3x3 grid) - for (int dy = -1; dy <= 1; ++dy) { - for (int dx = -1; dx <= 1; ++dx) { - gatherLightsFromNeighborChunk( - map, chunk_x + dx, chunk_y + dy, - client_data, floor, floor_offset); + + gatherLightsForViewportFloor( + map, client_data, current_floor, floor, light_buffer); + } +} + +void LightGatherer::registerGroundOcclusionForViewport( + const Domain::ChunkedMap& map, + Services::ClientDataService* client_data, + int16_t current_floor, + int16_t floor, + uint32_t floor_light_start, + ViewportLightBuffer& light_buffer) +{ + const int32_t floor_offset = projectedFloorOffsetTiles(current_floor, floor); + const int32_t min_x = light_buffer.origin_x + floor_offset; + const int32_t min_y = light_buffer.origin_y + floor_offset; + const int32_t max_x = light_buffer.origin_x + light_buffer.width - 1 + floor_offset; + const int32_t max_y = light_buffer.origin_y + light_buffer.height - 1 + floor_offset; + + std::vector chunks; + map.getVisibleChunks(min_x, min_y, max_x, max_y, floor, chunks); + + for (Domain::Chunk* chunk : chunks) { + if (!chunk) continue; + + chunk->forEachTile([&](const Domain::Tile* tile) { + if (!tile || tile->getZ() != floor) return; + + const Domain::Item* ground = tile->getGround(); + if (!ground) return; + + const Domain::ItemType* ground_type = ground->getType(); + if (!ground_type) { + ground_type = client_data->getItemTypeByServerId(ground->getServerId()); } - } + if (!blocksLightFromBelow(ground_type)) return; + + const int32_t projected_x = tile->getX() - floor_offset; + const int32_t projected_y = tile->getY() - floor_offset; + light_buffer.setTileStart(projected_x, projected_y, floor_light_start); + }); } } -void LightGatherer::gatherLightsFromNeighborChunk( +void LightGatherer::gatherLightsForViewportFloor( const Domain::ChunkedMap& map, - int32_t target_cx, int32_t target_cy, Services::ClientDataService* client_data, - int16_t floor, int32_t floor_offset) + int16_t current_floor, + int16_t floor, + ViewportLightBuffer& light_buffer) { - int32_t tile_start_x = target_cx * Domain::Chunk::SIZE; - int32_t tile_start_y = target_cy * Domain::Chunk::SIZE; - int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; - int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; - + const int32_t floor_offset = projectedFloorOffsetTiles(current_floor, floor); + const int32_t min_x = + light_buffer.origin_x + floor_offset - LIGHT_COLLECTION_MARGIN_TILES; + const int32_t min_y = + light_buffer.origin_y + floor_offset - LIGHT_COLLECTION_MARGIN_TILES; + const int32_t max_x = + light_buffer.origin_x + light_buffer.width - 1 + floor_offset + + LIGHT_COLLECTION_MARGIN_TILES; + const int32_t max_y = + light_buffer.origin_y + light_buffer.height - 1 + floor_offset + + LIGHT_COLLECTION_MARGIN_TILES; + std::vector chunks; - map.getVisibleChunks(tile_start_x, tile_start_y, tile_end_x - 1, tile_end_y - 1, floor, chunks); - + map.getVisibleChunks(min_x, min_y, max_x, max_y, floor, chunks); + + const auto add_item_light = [&](const Domain::Item* item, + int32_t projected_x, + int32_t projected_y) { + if (!item) return; + const Domain::ItemType* item_type = item->getType(); + if (!item_type) { + item_type = client_data->getItemTypeByServerId(item->getServerId()); + } + if (item_type && item_type->light_level > 0) { + addLight(light_buffer, projected_x, projected_y, + item_type->light_color, item_type->light_level, floor); + } + }; + for (Domain::Chunk* chunk : chunks) { if (!chunk) continue; - + chunk->forEachTile([&](const Domain::Tile* tile) { if (!tile || tile->getZ() != floor) return; - - int32_t tile_x = tile->getX(); - int32_t tile_y = tile->getY(); - - // Apply isometric offset (RME-style: lights from higher floors - // are projected onto 2D with offset) - int32_t adjusted_x = tile_x - floor_offset; - int32_t adjusted_y = tile_y - floor_offset; - - // Helper lambda to add light - auto addLightFromItem = [&](const Domain::Item* item) { - if (!item) return; - const Domain::ItemType* item_type = client_data->getItemTypeByServerId(item->getServerId()); - if (item_type && item_type->light_level > 0) { - lights_.emplace_back(Domain::LightSource{ - .x = adjusted_x, - .y = adjusted_y, - .color = item_type->light_color, - .intensity = item_type->light_level - }); + + const int32_t projected_x = tile->getX() - floor_offset; + const int32_t projected_y = tile->getY() - floor_offset; + + add_item_light(tile->getGround(), projected_x, projected_y); + + for (const auto& item : tile->getItems()) { + add_item_light(item.get(), projected_x, projected_y); + } + + const Domain::Creature* creature = tile->getCreature(); + if (creature) { + const Domain::CreatureType* creature_type = + client_data->getCreatureType(creature->name); + if (creature_type && creature_type->light_level > 0) { + addLight(light_buffer, projected_x, projected_y, + creature_type->light_color, + creature_type->light_level, floor); } - }; - - // Check ground item - addLightFromItem(tile->getGround()); - - // Check all items on the tile - for (const auto& item_ptr : tile->getItems()) { - addLightFromItem(item_ptr.get()); } }); } + + if (floor == Config::Map::GROUND_LAYER + 1) { + const int16_t above_floor = Config::Map::GROUND_LAYER; + const int32_t above_offset = projectedFloorOffsetTiles(current_floor, above_floor); + const int32_t above_min_x = + light_buffer.origin_x + above_offset - LIGHT_COLLECTION_MARGIN_TILES; + const int32_t above_min_y = + light_buffer.origin_y + above_offset - LIGHT_COLLECTION_MARGIN_TILES; + const int32_t above_max_x = + light_buffer.origin_x + light_buffer.width - 1 + above_offset + + LIGHT_COLLECTION_MARGIN_TILES; + const int32_t above_max_y = + light_buffer.origin_y + light_buffer.height - 1 + above_offset + + LIGHT_COLLECTION_MARGIN_TILES; + + std::vector above_chunks; + map.getVisibleChunks(above_min_x, above_min_y, above_max_x, above_max_y, + above_floor, above_chunks); + + for (Domain::Chunk* chunk : above_chunks) { + if (!chunk) continue; + + chunk->forEachTile([&](const Domain::Tile* tile) { + if (!tile || tile->getZ() != above_floor) return; + if (!carriesTranslucentLight(tile, client_data)) return; + + const int32_t projected_x = tile->getX() - floor_offset; + const int32_t projected_y = tile->getY() - floor_offset; + addLight(light_buffer, projected_x, projected_y, + Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR, + Config::Lighting::TRANSLUCENT_PROPAGATION_INTENSITY, + floor); + }); + } + } } } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Light/LightGatherer.h b/ImguiMapEditor/Rendering/Light/LightGatherer.h index edef1ef..1ead3db 100644 --- a/ImguiMapEditor/Rendering/Light/LightGatherer.h +++ b/ImguiMapEditor/Rendering/Light/LightGatherer.h @@ -12,6 +12,46 @@ namespace Services { namespace Rendering { +/** + * RME-style viewport light buffer. + * + * All lights across all processed floors are appended to one vector in + * top-to-bottom floor order. Each viewport tile stores the first light index + * allowed to affect it; lower indices were collected from floors above a solid + * ground tile and are blocked for that projected tile position. + */ +struct ViewportLightBuffer { + std::vector lights; + std::vector tile_start; + int width = 0; + int height = 0; + int origin_x = 0; + int origin_y = 0; + + void init(int ox, int oy, int w, int h) { + origin_x = ox; + origin_y = oy; + width = w; + height = h; + lights.clear(); + tile_start.assign(w * h, 0); + } + + uint32_t getTileStart(int tx, int ty) const { + int lx = tx - origin_x; + int ly = ty - origin_y; + if (lx < 0 || lx >= width || ly < 0 || ly >= height) return 0; + return tile_start[ly * width + lx]; + } + + void setTileStart(int tx, int ty, uint32_t start) { + int lx = tx - origin_x; + int ly = ty - origin_y; + if (lx < 0 || lx >= width || ly < 0 || ly >= height) return; + tile_start[ly * width + lx] = start; + } +}; + /** * Collects light sources from visible tiles. * @@ -21,59 +61,36 @@ namespace Rendering { class LightGatherer { public: /** - * Clear all collected lights for a new frame. - */ - void clear(); - - /** - * Gather all light sources relevant to a specific chunk. - * scans the target chunk AND its 8 neighbors (3x3 grid) to account for light spilling. - */ - void gatherForChunk( - const MapEditor::Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, - Services::ClientDataService* client_data, - int16_t floor); - - /** - * Gather all light sources from multiple floors for a specific chunk. - * Applies isometric offset to light positions based on floor difference. - * - * @param map The map to gather lights from - * @param chunk_x Chunk X coordinate - * @param chunk_y Chunk Y coordinate - * @param client_data Client data service for item type lookup - * @param start_floor First floor to gather from (highest Z) - * @param end_floor Last floor to gather from (lowest Z) + * Gather lights and ground occlusion into a single RME-style viewport buffer. */ - void gatherForChunkMultiFloor( + void gatherViewportLightBuffer( const MapEditor::Domain::ChunkedMap& map, - int32_t chunk_x, int32_t chunk_y, Services::ClientDataService* client_data, + int16_t current_floor, int16_t start_floor, - int16_t end_floor); + int16_t end_floor, + ViewportLightBuffer& light_buffer); - /** - * Get the collected light sources. - */ - const std::vector& getLights() const { return lights_; } - - /** - * Get number of light sources collected. - */ - size_t getLightCount() const { return lights_.size(); } - private: - /** - * Helper to gather lights from a single neighbor chunk on a specific floor. - */ - void gatherLightsFromNeighborChunk( + void addLight(ViewportLightBuffer& light_buffer, + int32_t x, int32_t y, + uint8_t color, uint8_t intensity, + int16_t floor); + + void registerGroundOcclusionForViewport( const MapEditor::Domain::ChunkedMap& map, - int32_t target_cx, int32_t target_cy, Services::ClientDataService* client_data, - int16_t floor, int32_t floor_offset); + int16_t current_floor, + int16_t floor, + uint32_t floor_light_start, + ViewportLightBuffer& light_buffer); - std::vector lights_; + void gatherLightsForViewportFloor( + const MapEditor::Domain::ChunkedMap& map, + Services::ClientDataService* client_data, + int16_t current_floor, + int16_t floor, + ViewportLightBuffer& light_buffer); }; } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Light/LightManager.cpp b/ImguiMapEditor/Rendering/Light/LightManager.cpp index e913327..762af1d 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.cpp +++ b/ImguiMapEditor/Rendering/Light/LightManager.cpp @@ -1,10 +1,15 @@ #include "LightManager.h" -#include "LightColorPalette.h" -#include "Services/ClientDataService.h" -#include + #include +#include + #include -#include // For memcpy + +#include "LightColorPalette.h" +#include "Core/Config.h" +#include "Rendering/Light/LightProjection.h" +#include "Services/ClientDataService.h" +#include "Rendering/Visibility/FloorVisibilityCalculator.h" namespace MapEditor { namespace Rendering { @@ -17,7 +22,6 @@ LightManager::LightManager(Services::ClientDataService* client_data) LightManager::~LightManager() = default; bool LightManager::initialize() { - cache_ = std::make_unique(); texture_ = std::make_unique(); overlay_ = std::make_unique(); gatherer_ = std::make_unique(); @@ -35,31 +39,35 @@ bool LightManager::initialize() { } void LightManager::invalidateTile(int32_t x, int32_t y) { - // Flag that we need to update the light texture next frame + (void)x; + (void)y; force_update_ = true; - - if (cache_) { - // Invalidate chunk containing the tile AND its neighbors - // to propagate light changes (spilling) - - // Using explicit shift to match ChunkedMap - int32_t cx = x >> 5; - int32_t cy = y >> 5; - - // Since we don't know the floor here yet (interface limitation), - // valid safe approach is to invalidate this column on all floors. - // Most map edits are single floor, but 16 iterations is cheap. - for (int16_t z = 0; z <= 15; ++z) { - cache_->invalidateRegion(cx - 1, cy - 1, cx + 1, cy + 1, z); - } - } } void LightManager::invalidateAll() { force_update_ = true; - if (cache_) { - cache_->clear(); - } +} + +void LightManager::renderClientVisible( + const Domain::ChunkedMap& map, + int viewport_width, int viewport_height, + float camera_x, float camera_y, + float zoom, int current_floor, + const Domain::LightConfig& config, + std::optional visibility_origin, + const std::vector* injected_lights) +{ + FloorVisibilityCalculator floor_visibility(client_data_); + const int visibility_x = + visibility_origin ? visibility_origin->x : static_cast(camera_x); + const int visibility_y = + visibility_origin ? visibility_origin->y : static_cast(camera_y); + const int start_floor = floor_visibility.calcLastVisibleFloor(current_floor); + const int end_floor = floor_visibility.calcFirstVisibleFloor( + map, visibility_x, visibility_y, current_floor); + + render(map, viewport_width, viewport_height, camera_x, camera_y, zoom, + current_floor, start_floor, end_floor, config, injected_lights); } void LightManager::render(const Domain::ChunkedMap& map, @@ -67,7 +75,8 @@ void LightManager::render(const Domain::ChunkedMap& map, float camera_x, float camera_y, float zoom, int current_floor, int start_floor, int end_floor, - const Domain::LightConfig& config) + const Domain::LightConfig& config, + const std::vector* injected_lights) { if (!config.enabled) return; @@ -96,14 +105,19 @@ void LightManager::render(const Domain::ChunkedMap& map, end_floor != last_end_floor_; bool config_changed = - config.ambient_color != last_config_.ambient_color || - config.ambient_level != last_config_.ambient_level || + config.global_light.color != last_config_.global_light.color || + config.global_light.intensity != last_config_.global_light.intensity || + config.client_slider != last_config_.client_slider || + config.camera_floor != last_config_.camera_floor || config.enabled != last_config_.enabled; - // If bounds and config match, we can reuse the texture + const bool has_injected_lights = injected_lights && !injected_lights->empty(); + + // If bounds and config match, we can reuse the texture. + // Injected lights are dynamic preview/player lights, so they always recompute. // But we MUST recalculate screen coordinates because sub-pixel camera movement // changes where the texture is drawn, even if the integer tile range is same. - if (!bounds_changed && !config_changed && !force_update_) { + if (!has_injected_lights && !bounds_changed && !config_changed && !force_update_) { float world_x = start_x * 32.0f; float world_y = start_y * 32.0f; float screen_x = (world_x - camera_x * 32.0f) * zoom + viewport_width / 2.0f; @@ -128,73 +142,43 @@ void LightManager::render(const Domain::ChunkedMap& map, last_config_ = config; force_update_ = false; - // 2. Prepare buffer + // 2. Gather one RME-style viewport light buffer and compute brightness globally. size_t required_size = width_tiles * height_tiles * 4; if (viewport_buffer_.size() < required_size) { viewport_buffer_.resize(required_size); } - - // 3. Iterate over chunks in the view - // To optimized cache usage, iterate by chunks visible - int chunk_start_x = start_x >> 5; - int chunk_start_y = start_y >> 5; - int chunk_end_x = end_x >> 5; - int chunk_end_y = end_y >> 5; - - for (int cy = chunk_start_y; cy <= chunk_end_y; ++cy) { - for (int cx = chunk_start_x; cx <= chunk_end_x; ++cx) { - - // Get or compute the grid - // Use current_floor as cache key since all lights are projected to this floor - CachedLightGrid& grid = cache_->getOrCreateGrid(cx, cy, static_cast(current_floor)); - if (!grid.is_valid) { - gatherer_->clear(); - - // Use multi-floor gathering if we have a floor range - if (start_floor != end_floor) { - gatherer_->gatherForChunkMultiFloor( - map, cx, cy, client_data_, - static_cast(start_floor), - static_cast(end_floor)); - } else { - // Single floor mode - gatherer_->gatherForChunk(map, cx, cy, client_data_, static_cast(current_floor)); + + light_buffer_.init(start_x, start_y, width_tiles, height_tiles); + gatherer_->gatherViewportLightBuffer( + map, client_data_, static_cast(current_floor), + static_cast(start_floor), static_cast(end_floor), + light_buffer_); + + if (injected_lights) { + for (const auto& light : *injected_lights) { + Domain::LightSource projected = light; + const int32_t floor_offset = projectedFloorOffsetTiles( + static_cast(current_floor), light.source_floor); + projected.x -= floor_offset; + projected.y -= floor_offset; + + if (!light_buffer_.lights.empty()) { + auto& prev = light_buffer_.lights.back(); + if (prev.x == projected.x && prev.y == projected.y && + prev.color == projected.color && + prev.source_floor == projected.source_floor) { + prev.intensity = std::max(prev.intensity, projected.intensity); + continue; } - - computeChunkLight(grid, gatherer_->getLights(), config, cx, cy); - grid.is_valid = true; - } - - // Copy relevant part of the grid to viewport buffer - int chunk_pixel_x = cx * 32; - int chunk_pixel_y = cy * 32; - - // Intersection between Chunk and Viewport - int ix_start = std::max(start_x, chunk_pixel_x); - int ix_end = std::min(end_x, chunk_pixel_x + 32); - int iy_start = std::max(start_y, chunk_pixel_y); - int iy_end = std::min(end_y, chunk_pixel_y + 32); - - if (ix_start >= ix_end || iy_start >= iy_end) continue; - - // Copy loops - for (int y = iy_start; y < iy_end; ++y) { - int dest_y = y - start_y; - int src_y = y - chunk_pixel_y; - - int dest_row_start = (dest_y * width_tiles + (ix_start - start_x)) * 4; - int src_row_start = (src_y * 32 + (ix_start - chunk_pixel_x)) * 4; // Assuming 32 width - int row_len = (ix_end - ix_start) * 4; // Bytes - - // Safety check - std::memcpy(&viewport_buffer_[dest_row_start], - reinterpret_cast(&grid.pixels[0]) + src_row_start, - row_len); } + + light_buffer_.lights.push_back(projected); } } - // 4. Upload and Render + computeViewportLight(light_buffer_, config); + + // 3. Upload and Render texture_->upload(viewport_buffer_, width_tiles, height_tiles); // Calc screen rect @@ -211,54 +195,73 @@ void LightManager::render(const Domain::ChunkedMap& map, glm::vec2(viewport_width, viewport_height)); } -void LightManager::computeChunkLight(CachedLightGrid& grid, - const std::vector& lights, - const Domain::LightConfig& config, - int32_t chunk_x, int32_t chunk_y) +void LightManager::computeViewportLight(const ViewportLightBuffer& light_buffer, + const Domain::LightConfig& config) { - // OPTIMIZED: Iterate lights first, then only affected tiles - - // Get ambient light color + const bool above_ground = config.camera_floor <= Config::Map::GROUND_LAYER; + const uint8_t ambient_color = above_ground + ? config.global_light.color + : Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; + const uint8_t ambient_intensity = above_ground + ? std::max(config.client_slider, config.global_light.intensity) + : config.client_slider; + float ambient_r, ambient_g, ambient_b; - LightColorPalette::from8bitFloat(config.ambient_color, ambient_r, ambient_g, ambient_b); - - float ambient_scale = config.ambient_level / 255.0f; + LightColorPalette::from8bitFloat(ambient_color, ambient_r, ambient_g, ambient_b); + const float ambient_scale = static_cast(ambient_intensity) / 255.0f; ambient_r *= ambient_scale; ambient_g *= ambient_scale; ambient_b *= ambient_scale; - uint8_t base_r = static_cast(ambient_r * 255.0f); - uint8_t base_g = static_cast(ambient_g * 255.0f); - uint8_t base_b = static_cast(ambient_b * 255.0f); + uint8_t base_r = static_cast(std::min(ambient_r * 255.0f, 255.0f)); + uint8_t base_g = static_cast(std::min(ambient_g * 255.0f, 255.0f)); + uint8_t base_b = static_cast(std::min(ambient_b * 255.0f, 255.0f)); // Fill with ambient first - uint32_t ambient_packed = base_r | (base_g << 8) | (base_b << 16) | (255 << 24); - std::fill(grid.pixels.begin(), grid.pixels.end(), ambient_packed); - - int chunk_start_x = chunk_x * 32; - int chunk_start_y = chunk_y * 32; + const size_t pixel_count = + static_cast(light_buffer.width) * static_cast(light_buffer.height); + for (size_t i = 0; i < pixel_count; ++i) { + const size_t base = i * 4; + viewport_buffer_[base + 0] = base_r; + viewport_buffer_[base + 1] = base_g; + viewport_buffer_[base + 2] = base_b; + viewport_buffer_[base + 3] = 255; + } // Iterate lights FIRST, then only tiles affected by each light - for (const auto& light : lights) { + for (size_t light_index = 0; light_index < light_buffer.lights.size(); ++light_index) { + const auto& light = light_buffer.lights[light_index]; // Pre-compute light color once per light float lr, lg, lb; LightColorPalette::from8bitFloat(light.color, lr, lg, lb); - // Calculate bounding box of affected tiles (in local chunk coords) + // Calculate bounding box of affected tiles (in viewport-local coords) int radius = light.intensity; - int min_x = std::max(0, light.x - radius - chunk_start_x); - int max_x = std::min(31, light.x + radius - chunk_start_x); - int min_y = std::max(0, light.y - radius - chunk_start_y); - int max_y = std::min(31, light.y + radius - chunk_start_y); + int min_x = std::max(0, light.x - radius - light_buffer.origin_x); + int max_x = std::min(light_buffer.width - 1, + light.x + radius - light_buffer.origin_x); + int min_y = std::max(0, light.y - radius - light_buffer.origin_y); + int max_y = std::min(light_buffer.height - 1, + light.y + radius - light_buffer.origin_y); - // Skip if light doesn't affect this chunk - if (min_x > 31 || max_x < 0 || min_y > 31 || max_y < 0) continue; + // Skip if light doesn't affect this viewport + if (min_x >= light_buffer.width || max_x < 0 || + min_y >= light_buffer.height || max_y < 0) { + continue; + } // Only iterate tiles within light's bounding box for (int y = min_y; y <= max_y; ++y) { for (int x = min_x; x <= max_x; ++x) { - int tile_x = chunk_start_x + x; - int tile_y = chunk_start_y + y; + const size_t tile_index = + static_cast(y) * static_cast(light_buffer.width) + + static_cast(x); + if (light_index < light_buffer.tile_start[tile_index]) { + continue; + } + + int tile_x = light_buffer.origin_x + x; + int tile_y = light_buffer.origin_y + y; float dx = (static_cast(tile_x) + 0.5f) - (static_cast(light.x) + 0.5f); float dy = (static_cast(tile_y) + 0.5f) - (static_cast(light.y) + 0.5f); @@ -275,15 +278,18 @@ void LightManager::computeChunkLight(CachedLightGrid& grid, if (intensity > 1.0f) intensity = 1.0f; // Update pixel with max blending - uint32_t& pixel = grid.pixels[y * 32 + x]; - int r = std::max(static_cast(pixel & 0xFF), static_cast(lr * intensity * 255.0f)); - int g = std::max(static_cast((pixel >> 8) & 0xFF), static_cast(lg * intensity * 255.0f)); - int b = std::max(static_cast((pixel >> 16) & 0xFF), static_cast(lb * intensity * 255.0f)); - - pixel = static_cast(std::min(r, 255)) | - (static_cast(std::min(g, 255)) << 8) | - (static_cast(std::min(b, 255)) << 16) | - (255 << 24); + const size_t base = tile_index * 4; + int r = std::max(static_cast(viewport_buffer_[base + 0]), + static_cast(lr * intensity * 255.0f)); + int g = std::max(static_cast(viewport_buffer_[base + 1]), + static_cast(lg * intensity * 255.0f)); + int b = std::max(static_cast(viewport_buffer_[base + 2]), + static_cast(lb * intensity * 255.0f)); + + viewport_buffer_[base + 0] = static_cast(std::min(r, 255)); + viewport_buffer_[base + 1] = static_cast(std::min(g, 255)); + viewport_buffer_[base + 2] = static_cast(std::min(b, 255)); + viewport_buffer_[base + 3] = 255; } } } diff --git a/ImguiMapEditor/Rendering/Light/LightManager.h b/ImguiMapEditor/Rendering/Light/LightManager.h index 204688e..2d7fd73 100644 --- a/ImguiMapEditor/Rendering/Light/LightManager.h +++ b/ImguiMapEditor/Rendering/Light/LightManager.h @@ -1,11 +1,12 @@ #pragma once -#include "LightCache.h" #include "LightTexture.h" #include "LightOverlay.h" #include "LightGatherer.h" #include "Domain/ChunkedMap.h" #include "Domain/LightTypes.h" +#include "Domain/Position.h" #include +#include #include namespace MapEditor { @@ -32,13 +33,30 @@ class LightManager { * @param start_floor First floor to gather lights from (highest Z) * @param end_floor Last floor to gather lights from (lowest Z) * When start_floor == end_floor, only that floor is used + * @param injected_lights Optional extra lights to add (e.g., player light in preview) */ void render(const MapEditor::Domain::ChunkedMap& map, int viewport_width, int viewport_height, float camera_x, float camera_y, float zoom, int current_floor, int start_floor, int end_floor, - const MapEditor::Domain::LightConfig& config); + const MapEditor::Domain::LightConfig& config, + const std::vector* injected_lights = nullptr); + + /** + * Render using RME's light-mode floor visibility range. + * + * RME ignores the editor's show-all-floors range when collecting lights and + * instead asks the client visibility algorithm which floors are actually + * visible through ground/roof blockers. + */ + void renderClientVisible(const MapEditor::Domain::ChunkedMap& map, + int viewport_width, int viewport_height, + float camera_x, float camera_y, + float zoom, int current_floor, + const MapEditor::Domain::LightConfig& config, + std::optional visibility_origin = std::nullopt, + const std::vector* injected_lights = nullptr); /** * Invalidate light cache for a specific tile position. @@ -52,17 +70,15 @@ class LightManager { void invalidateAll(); private: - void computeChunkLight(CachedLightGrid& grid, - const std::vector& lights, - const MapEditor::Domain::LightConfig& config, - int32_t chunk_x, int32_t chunk_y); + void computeViewportLight(const ViewportLightBuffer& light_buffer, + const MapEditor::Domain::LightConfig& config); Services::ClientDataService* client_data_; - std::unique_ptr cache_; std::unique_ptr texture_; std::unique_ptr overlay_; std::unique_ptr gatherer_; + ViewportLightBuffer light_buffer_; std::vector viewport_buffer_; diff --git a/ImguiMapEditor/Rendering/Light/LightProjection.h b/ImguiMapEditor/Rendering/Light/LightProjection.h new file mode 100644 index 0000000..c4270c2 --- /dev/null +++ b/ImguiMapEditor/Rendering/Light/LightProjection.h @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "Core/Config.h" + +namespace MapEditor::Rendering { + +inline int32_t projectedFloorOffsetTiles(int16_t current_floor, + int16_t tile_floor) { + if (tile_floor <= Config::Map::GROUND_LAYER) { + return Config::Map::GROUND_LAYER - tile_floor; + } + return current_floor - tile_floor; +} + +} // namespace MapEditor::Rendering diff --git a/ImguiMapEditor/Rendering/Map/MapRenderer.cpp b/ImguiMapEditor/Rendering/Map/MapRenderer.cpp index 4bc34fd..8b4d7ba 100644 --- a/ImguiMapEditor/Rendering/Map/MapRenderer.cpp +++ b/ImguiMapEditor/Rendering/Map/MapRenderer.cpp @@ -79,6 +79,11 @@ void MapRenderer::setViewSettings(Services::ViewSettings *settings) { } } +void MapRenderer::setLightVisibilityOrigin( + std::optional origin) { + light_visibility_origin_ = origin; +} + void MapRenderer::setLODMode(bool enabled) { render_pipeline_.setLODMode(enabled); } @@ -131,7 +136,8 @@ void MapRenderer::render(const Domain::ChunkedMap &map, RenderState &state, base_bounds, // Visible bounds camera_.getFloor(), // Current floor frame_data_collector_.getMissingSpriteBuffer(), // Missing sprites buffer - view_settings_}; // View settings + view_settings_, // View settings + light_visibility_origin_}; // Light visibility origin // Execute Pipeline render_pipeline_.render(context); diff --git a/ImguiMapEditor/Rendering/Map/MapRenderer.h b/ImguiMapEditor/Rendering/Map/MapRenderer.h index 978b7e6..694844f 100644 --- a/ImguiMapEditor/Rendering/Map/MapRenderer.h +++ b/ImguiMapEditor/Rendering/Map/MapRenderer.h @@ -1,6 +1,7 @@ #pragma once #include "Core/Config.h" #include "Domain/ChunkedMap.h" +#include "Domain/Position.h" #include "Rendering/Animation/AnimationTicks.h" #include "Rendering/Backend/IRenderer.h" #include "Rendering/Backend/SpriteBatch.h" @@ -23,6 +24,7 @@ #include "Services/ViewSettings.h" #include #include +#include #include namespace MapEditor { @@ -85,6 +87,7 @@ class MapRenderer : public IRenderer { void setGridVisible(bool visible) { show_grid_ = visible; } bool isGridVisible() const { return show_grid_; } void setViewSettings(Services::ViewSettings *settings) override; + void setLightVisibilityOrigin(std::optional origin); /** * Set LOD mode to enable/disable simplified rendering and optimizations. @@ -155,6 +158,7 @@ class MapRenderer : public IRenderer { // Camera component (handles position, zoom, matrix) ViewCamera camera_; + std::optional light_visibility_origin_; bool show_grid_ = true; // Stats diff --git a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp index 18ef436..112f2cc 100644 --- a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp +++ b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp @@ -225,19 +225,35 @@ void IngamePreviewRenderer::render(const Domain::ChunkedMap &map, if (lighting_enabled && light_manager_ && client_data_) { Domain::LightConfig config; config.enabled = true; - config.ambient_level = - static_cast(view_settings->preview_ambient_light); - config.ambient_color = 215; - - if (config.ambient_level != last_ambient_light_) { + config.global_light.intensity = view_settings->preview_server_light_intensity; + config.global_light.color = view_settings->preview_server_light_color; + config.client_slider = static_cast(view_settings->preview_ambient_light); + config.camera_floor = floor; + config.ambient_color = config.global_light.color; + config.ambient_level = config.global_light.intensity; + + // Auto-invalidate if lighting config changes + uint32_t config_hash = config.computeHash(); + if (config_hash != last_config_hash_) { light_manager_->invalidateAll(); - last_ambient_light_ = config.ambient_level; + last_config_hash_ = config_hash; } + // Player minimum light at center tile (preview only). The color reuses + // Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR, matching OTClient's white light. + std::vector player_lights; + player_lights.push_back(Domain::LightSource{ + .x = static_cast(camera_x), + .y = static_cast(camera_y), + .color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR, + .intensity = Config::Lighting::DEFAULT_PLAYER_LIGHT_INTENSITY, + .source_floor = static_cast(floor) + }); + light_manager_->render(map, viewport_width, viewport_height, camera_x, camera_y, zoom, floor, - render_start_z, render_end_z, // Floor range from fading calculation - config); + render_start_z, render_end_z, + config, &player_lights); } } diff --git a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h index 30497a8..d8eddf0 100644 --- a/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h +++ b/ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h @@ -67,7 +67,7 @@ class IngamePreviewRenderer { // Light system (independent from MapRenderer) std::unique_ptr light_manager_; - uint8_t last_ambient_light_ = 255; + uint32_t last_config_hash_ = 0; // Fading state int last_player_z_ = -1; diff --git a/ImguiMapEditor/Rendering/Passes/LightingPass.cpp b/ImguiMapEditor/Rendering/Passes/LightingPass.cpp index e2c1aba..09cf0f7 100644 --- a/ImguiMapEditor/Rendering/Passes/LightingPass.cpp +++ b/ImguiMapEditor/Rendering/Passes/LightingPass.cpp @@ -1,7 +1,6 @@ #include "Rendering/Passes/LightingPass.h" #include "Rendering/Frame/RenderState.h" #include "Rendering/Light/LightManager.h" -#include "Rendering/Visibility/FloorIterator.h" #include "Services/ViewSettings.h" #include @@ -18,38 +17,29 @@ void LightingPass::render(const RenderContext &context) { return; } + // Build LightConfig from ViewSettings (new server light + client slider) Domain::LightConfig config; config.enabled = true; - config.ambient_level = - static_cast(context.view_settings->map_ambient_light); - config.ambient_color = 215; // Default white-ish (from MapRenderer.cpp) - - // Auto-invalidate if ambient light changes - // Note: RenderState tracking logic moved here - if (config.ambient_level != context.state.last_ambient_light) { - context.state.light_manager->invalidateAll(); - context.state.last_ambient_light = config.ambient_level; - } - - // Calculate floor range based on show_all_floors setting - bool show_all_floors = context.view_settings->show_all_floors; - FloorRange floor_range = FloorIterator::calculateRangeWithToggle( - context.current_floor, show_all_floors); - - // Invalidate if floor range changed (e.g., toggling show_all_floors) - if (floor_range.start_z != last_start_floor_ || - floor_range.super_end_z != last_end_floor_) { + config.global_light.intensity = context.view_settings->server_light_intensity; + config.global_light.color = context.view_settings->server_light_color; + config.client_slider = static_cast(context.view_settings->map_ambient_light); + config.camera_floor = context.current_floor; + // Legacy fields (kept for backward compat) + config.ambient_color = config.global_light.color; + config.ambient_level = config.global_light.intensity; + + // Auto-invalidate if lighting config changes + uint32_t config_hash = config.computeHash(); + if (config_hash != context.state.last_config_hash) { context.state.light_manager->invalidateAll(); - last_start_floor_ = floor_range.start_z; - last_end_floor_ = floor_range.super_end_z; + context.state.last_config_hash = config_hash; } - context.state.light_manager->render( + context.state.light_manager->renderClientVisible( context.map, context.viewport_width, context.viewport_height, context.camera.getX(), context.camera.getY(), context.camera.getZoom(), static_cast(context.current_floor), - floor_range.start_z, floor_range.super_end_z, - config); + config, context.light_visibility_origin); } } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Passes/LightingPass.h b/ImguiMapEditor/Rendering/Passes/LightingPass.h index e1b5234..36391e8 100644 --- a/ImguiMapEditor/Rendering/Passes/LightingPass.h +++ b/ImguiMapEditor/Rendering/Passes/LightingPass.h @@ -14,10 +14,6 @@ class LightingPass : public IRenderPass { ~LightingPass() override = default; void render(const RenderContext &context) override; - -private: - int last_start_floor_ = -1; - int last_end_floor_ = -1; }; } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp b/ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp index ba1effe..17058fc 100644 --- a/ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp +++ b/ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp @@ -12,72 +12,59 @@ FloorVisibilityCalculator::FloorVisibilityCalculator(Services::ClientDataService const Domain::ItemType* FloorVisibilityCalculator::getItemType(const Domain::Item* item) const { if (!item || !client_data_) return nullptr; + if (const Domain::ItemType* item_type = item->getType()) { + return item_type; + } return client_data_->getItemTypeByServerId(item->getServerId()); } bool FloorVisibilityCalculator::tileLimitsFloorsView(const Domain::Tile* tile, bool is_free_view) const { if (!tile) return false; - - // Check ground first - const Domain::Item* ground = tile->getGround(); - if (!ground) return false; - - const Domain::ItemType* ground_type = getItemType(ground); - if (!ground_type) return false; - - // Items with isDontHide never block view - if (ground_type->is_dont_hide) return false; - - // Ground tiles block view - if (ground_type->is_ground) return true; - - // isOnBottom items (walls) block view - if (ground_type->is_on_bottom) { - if (is_free_view) { - // Free view: any wall blocks - return true; - } else { - // Player view: only walls that block projectiles - return ground_type->blocks_projectile; - } + + const Domain::Item* first_thing = tile->getGround(); + if (!first_thing && !tile->getItems().empty()) { + first_thing = tile->getItems().front().get(); } - - // Check other items on tile for blocking - for (const auto& item : tile->getItems()) { - const Domain::ItemType* item_type = getItemType(item.get()); - if (!item_type) continue; - - if (item_type->is_dont_hide) continue; - - if (item_type->is_ground) return true; - - if (item_type->is_on_bottom) { - if (is_free_view) { - return true; - } else { - if (item_type->blocks_projectile) return true; - } - } + + const Domain::ItemType* item_type = getItemType(first_thing); + if (!item_type) return false; + + if (item_type->hasFlag(Domain::ItemFlag::IgnoreLook)) { + return false; } - - return false; + + const bool is_ground_tile = item_type->isGround() || item_type->is_ground; + const bool is_bottom = item_type->always_on_bottom || item_type->is_on_bottom; + const bool blocks_projectile = + item_type->blocks_projectile || + item_type->hasFlag(Domain::ItemFlag::BlockMissiles); + + if (is_free_view) { + return is_ground_tile || is_bottom; + } + + return is_ground_tile || (is_bottom && blocks_projectile); } bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { - if (!tile) return true; // Empty tile is transparent + if (!tile) return false; // Check all items for projectile blocking const Domain::Item* ground = tile->getGround(); if (ground) { const Domain::ItemType* ground_type = getItemType(ground); - if (ground_type && ground_type->blocks_projectile) { + if (ground_type && + (ground_type->blocks_projectile || + ground_type->hasFlag(Domain::ItemFlag::BlockMissiles))) { return false; } } for (const auto& item : tile->getItems()) { const Domain::ItemType* item_type = getItemType(item.get()); - if (item_type && item_type->blocks_projectile) { + if (item_type && + (item_type->blocks_projectile || + item_type->hasFlag(Domain::ItemFlag::BlockMissiles))) { return false; } } @@ -99,48 +86,44 @@ int FloorVisibilityCalculator::calcFirstVisibleFloor( static_cast(FC::UNDERGROUND_FLOOR)); } - // Check 3x3 area around camera for blocking tiles for (int ix = -1; ix <= 1 && first_floor < camera_z; ++ix) { for (int iy = -1; iy <= 1 && first_floor < camera_z; ++iy) { - int pos_x = camera_x + ix; - int pos_y = camera_y + iy; - - // Center tile OR diagonal tiles where we can look through - bool is_center = (ix == 0 && iy == 0); - bool is_diagonal = (std::abs(ix) == std::abs(iy)) && !is_center; - - // For diagonal tiles, check if we can look through (window/door) - if (!is_center && is_diagonal) { - const Domain::Tile* current_tile = map.getTile(pos_x, pos_y, camera_z); - if (!isLookPossible(current_tile)) { - continue; // Can't look diagonally through solid tiles - } + const int pos_x = camera_x + ix; + const int pos_y = camera_y + iy; + const bool is_center = ix == 0 && iy == 0; + const bool is_straight_neighbor = std::abs(ix) != std::abs(iy); + const Domain::Tile* position_tile = map.getTile(pos_x, pos_y, camera_z); + const bool look_possible = isLookPossible(position_tile); + + if (!is_center && (!is_straight_neighbor || !look_possible)) { + continue; } - - // Walk up through floors checking for blockers - // OTClient uses coveredUp which shifts x+1, y+1, z-1 - int check_x = pos_x; - int check_y = pos_y; - - for (int check_z = camera_z - 1; check_z >= first_floor; --check_z) { - // Apply covered position shift (each floor up = x+1, y+1) - int z_diff = camera_z - check_z; - int covered_x = pos_x + z_diff; - int covered_y = pos_y + z_diff; - - // Check tile directly above (physical position) - const Domain::Tile* upper_tile = map.getTile(pos_x, pos_y, check_z); - bool can_look = isLookPossible(map.getTile(pos_x, pos_y, camera_z)); - - if (upper_tile && tileLimitsFloorsView(upper_tile, !can_look)) { - first_floor = check_z + 1; + + int upper_x = pos_x; + int upper_y = pos_y; + int upper_z = camera_z; + int covered_x = pos_x; + int covered_y = pos_y; + int covered_z = camera_z; + + while (upper_z > 0 && covered_z > 0) { + --upper_z; + --covered_z; + ++covered_x; + ++covered_y; + if (upper_z < first_floor) { break; } - - // Check tile geometrically above (covered position) - const Domain::Tile* covered_tile = map.getTile(covered_x, covered_y, check_z); - if (covered_tile && tileLimitsFloorsView(covered_tile, can_look)) { - first_floor = check_z + 1; + + if (const Domain::Tile* upper_tile = map.getTile(upper_x, upper_y, upper_z); + tileLimitsFloorsView(upper_tile, !look_possible)) { + first_floor = upper_z + 1; + break; + } + + if (const Domain::Tile* covered_tile = map.getTile(covered_x, covered_y, covered_z); + tileLimitsFloorsView(covered_tile, look_possible)) { + first_floor = covered_z + 1; break; } } diff --git a/ImguiMapEditor/Services/ClientDataService.cpp b/ImguiMapEditor/Services/ClientDataService.cpp index 92ac636..1b06322 100644 --- a/ImguiMapEditor/Services/ClientDataService.cpp +++ b/ImguiMapEditor/Services/ClientDataService.cpp @@ -349,6 +349,9 @@ void ClientDataService::mergeOtbWithDat( // Translucency — DAT flag already false for pre-10.00 items merged.is_translucent = dat->is_translucent; + // Lens help — items with value > 0 are see-through for light propagation + merged.lens_help = dat->lens_help; + // Ground speed from DAT if (dat->is_ground && dat->ground_speed > 0) { merged.speed = dat->ground_speed; diff --git a/ImguiMapEditor/Services/ViewSettings.cpp b/ImguiMapEditor/Services/ViewSettings.cpp index 0109ceb..dd0d05e 100644 --- a/ImguiMapEditor/Services/ViewSettings.cpp +++ b/ImguiMapEditor/Services/ViewSettings.cpp @@ -32,10 +32,23 @@ void ViewSettings::loadFromConfig(const ConfigService &config) { // Lighting Settings map_lighting_enabled = config.get("view.map_lighting_enabled", false); - map_ambient_light = config.get("view.map_ambient_light", 255); + map_ambient_light = config.get( + "view.map_ambient_light", Config::Lighting::DEFAULT_MINIMUM_AMBIENT); preview_lighting_enabled = config.get("view.preview_lighting_enabled", false); - preview_ambient_light = config.get("view.preview_ambient_light", 255); + preview_ambient_light = config.get( + "view.preview_ambient_light", Config::Lighting::DEFAULT_MINIMUM_AMBIENT); + server_light_intensity = static_cast(config.get( + "view.server_light_intensity", + Config::Lighting::DEFAULT_SERVER_LIGHT_INTENSITY)); + server_light_color = static_cast(config.get( + "view.server_light_color", Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR)); + preview_server_light_intensity = static_cast(config.get( + "view.preview_server_light_intensity", + Config::Lighting::DEFAULT_SERVER_LIGHT_INTENSITY)); + preview_server_light_color = static_cast(config.get( + "view.preview_server_light_color", + Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR)); show_minimap_window = config.get("view.show_minimap_window", false); show_browse_tile = config.get("view.show_browse_tile", false); @@ -78,6 +91,10 @@ void ViewSettings::saveToConfig(ConfigService &config) const { config.set("view.map_ambient_light", map_ambient_light); config.set("view.preview_lighting_enabled", preview_lighting_enabled); config.set("view.preview_ambient_light", preview_ambient_light); + config.set("view.server_light_intensity", static_cast(server_light_intensity)); + config.set("view.server_light_color", static_cast(server_light_color)); + config.set("view.preview_server_light_intensity", static_cast(preview_server_light_intensity)); + config.set("view.preview_server_light_color", static_cast(preview_server_light_color)); config.set("view.show_minimap_window", show_minimap_window); config.set("view.show_browse_tile", show_browse_tile); diff --git a/ImguiMapEditor/Services/ViewSettings.h b/ImguiMapEditor/Services/ViewSettings.h index be22e5e..aede411 100644 --- a/ImguiMapEditor/Services/ViewSettings.h +++ b/ImguiMapEditor/Services/ViewSettings.h @@ -44,9 +44,16 @@ struct ViewSettings { // === Lighting Settings === bool map_lighting_enabled = false; // Enable lighting in main map viewport - int map_ambient_light = 128; // 0 = dark, 255 = full bright + int map_ambient_light = Config::Lighting::DEFAULT_MINIMUM_AMBIENT; bool preview_lighting_enabled = false; // Enable lighting in ingame preview - int preview_ambient_light = 128; // 0 = dark, 255 = full bright + int preview_ambient_light = Config::Lighting::DEFAULT_MINIMUM_AMBIENT; + uint8_t server_light_intensity = + Config::Lighting::DEFAULT_SERVER_LIGHT_INTENSITY; + uint8_t server_light_color = Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; + uint8_t preview_server_light_intensity = + Config::Lighting::DEFAULT_SERVER_LIGHT_INTENSITY; + uint8_t preview_server_light_color = + Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR; // === Placeholders (menu only, no rendering yet) === bool show_minimap_window = false; diff --git a/ImguiMapEditor/UI/Map/MapPanel.cpp b/ImguiMapEditor/UI/Map/MapPanel.cpp index 647f742..6d11a2c 100644 --- a/ImguiMapEditor/UI/Map/MapPanel.cpp +++ b/ImguiMapEditor/UI/Map/MapPanel.cpp @@ -81,6 +81,7 @@ void MapPanel::renderInternal(MapType *map, Rendering::RenderState &state, // Update renderer camera renderer->setCameraPosition(camera_.getCameraPosition().x, camera_.getCameraPosition().y); + renderer->setLightVisibilityOrigin(input_.getLightVisibilityOrigin()); // Pass selection provider to renderer (session owns the adapter) if (session_) { diff --git a/ImguiMapEditor/UI/Map/MapPanelInput.cpp b/ImguiMapEditor/UI/Map/MapPanelInput.cpp index c002470..fd6ffe8 100644 --- a/ImguiMapEditor/UI/Map/MapPanelInput.cpp +++ b/ImguiMapEditor/UI/Map/MapPanelInput.cpp @@ -48,6 +48,7 @@ void MapPanelInput::handlePasteMode(MapViewCamera &camera, if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { glm::vec2 mouse_pos(io.MousePos.x, io.MousePos.y); Domain::Position target_pos = camera.screenToTile(mouse_pos); + setLightVisibilityOrigin(target_pos); bool replace_mode = session->isPasteReplaceMode() || io.KeyShift; session->confirmPaste(target_pos, replace_mode); return; @@ -108,8 +109,17 @@ void MapPanelInput::handleMouseZoom(MapViewCamera &camera) { } } +void MapPanelInput::setLightVisibilityOrigin(const Domain::Position &position) { + light_visibility_origin_ = position; +} + +void MapPanelInput::clearLightVisibilityOrigin() { + light_visibility_origin_.reset(); +} + void MapPanelInput::handleFloorChange(MapViewCamera &camera, bool is_focused) { ImGuiIO &io = ImGui::GetIO(); + const int16_t previous_floor = camera.getCurrentFloor(); // Ctrl + Scroll for floor change (inverted: scroll down = floor up, scroll up // = floor down) @@ -128,6 +138,10 @@ void MapPanelInput::handleFloorChange(MapViewCamera &camera, bool is_focused) { if (ImGui::IsKeyPressed(ImGuiKey_PageDown)) camera.floorDown(); } + + if (camera.getCurrentFloor() != previous_floor) { + clearLightVisibilityOrigin(); + } } bool MapPanelInput::shouldShowBoxOverlay() const { @@ -211,6 +225,7 @@ void MapPanelInput::handleTileSelection( // Handle Lasso Start with Alt Key if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + setLightVisibilityOrigin(camera.screenToTile(mouse_pos)); if (io.KeyAlt) { lasso_mode_ = LassoMode::Drawing; lasso_points_.clear(); @@ -341,6 +356,7 @@ void MapPanelInput::handleNormalSelectionInput( // Start tracking on left click (non-lasso) if (is_hovered && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { + setLightVisibilityOrigin(tile_pos); handleSelectionMouseDown(camera, session, input_controller, mouse_pos, tile_pos, mods, io); } diff --git a/ImguiMapEditor/UI/Map/MapPanelInput.h b/ImguiMapEditor/UI/Map/MapPanelInput.h index 35ef158..ce223c4 100644 --- a/ImguiMapEditor/UI/Map/MapPanelInput.h +++ b/ImguiMapEditor/UI/Map/MapPanelInput.h @@ -2,6 +2,7 @@ #include "Domain/Position.h" #include "MapViewCamera.h" #include +#include #include struct ImGuiIO; // Forward declaration @@ -60,6 +61,9 @@ class MapPanelInput { bool shouldShowLassoOverlay() const; const std::vector &getLassoPoints() const { return lasso_points_; } glm::vec2 getCurrentMousePos() const { return current_mouse_pos_; } + std::optional getLightVisibilityOrigin() const { + return light_visibility_origin_; + } // Context menu state bool shouldShowContextMenu() const { return show_context_menu_; } @@ -68,6 +72,8 @@ class MapPanelInput { private: void handlePasteMode(MapViewCamera &camera, AppLogic::EditorSession *session); + void setLightVisibilityOrigin(const Domain::Position &position); + void clearLightVisibilityOrigin(); void handleMousePan(MapViewCamera &camera, bool is_focused); void handleMouseZoom(MapViewCamera &camera); @@ -124,6 +130,7 @@ class MapPanelInput { // Panning state bool is_panning_ = false; glm::vec2 pan_start_{0, 0}; + std::optional light_visibility_origin_; // Drag selection state bool is_drag_selecting_ = false; diff --git a/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp b/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp new file mode 100644 index 0000000..d5ce503 --- /dev/null +++ b/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp @@ -0,0 +1,158 @@ +#include "LightColorPalettePicker.h" + +#include + +#include + +#include "Core/Config.h" +#include "Rendering/Light/LightColorPalette.h" + +namespace MapEditor::UI { +namespace { + +constexpr int kPaletteColumns = 18; +constexpr float kCellSize = 14.0f; +constexpr float kCellPadding = 2.0f; +constexpr float kButtonSwatchWidth = 24.0f; +constexpr float kButtonSwatchHeight = 16.0f; + +ImU32 paletteColor(uint8_t color_index) { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + Rendering::LightColorPalette::from8bit(color_index, r, g, b); + return IM_COL32(r, g, b, 255); +} + +ImU32 borderColorFor(ImU32 color) { + const int r = static_cast(color & 0xFF); + const int g = static_cast((color >> 8) & 0xFF); + const int b = static_cast((color >> 16) & 0xFF); + const int luma = (r * 299 + g * 587 + b * 114) / 1000; + return luma < 96 ? IM_COL32(200, 200, 200, 180) : IM_COL32(48, 48, 48, 180); +} + +void drawSwatch(ImDrawList& draw_list, + ImVec2 min, + ImVec2 max, + uint8_t color_index, + bool selected, + bool hovered) { + const ImU32 color = paletteColor(color_index); + draw_list.AddRectFilled(min, max, color); + draw_list.AddRect(min, max, borderColorFor(color)); + + if (selected) { + draw_list.AddRect(ImVec2(min.x - 2.0f, min.y - 2.0f), + ImVec2(max.x + 2.0f, max.y + 2.0f), + ImGui::GetColorU32(ImGuiCol_NavHighlight), 0.0f, 0, 2.0f); + } else if (hovered) { + draw_list.AddRect(ImVec2(min.x - 1.0f, min.y - 1.0f), + ImVec2(max.x + 1.0f, max.y + 1.0f), + IM_COL32(255, 255, 255, 128)); + } +} + +void drawCurrentColorButton(const char* id, uint8_t color, bool show_index) { + const float height = ImGui::GetFrameHeight(); + const float width = show_index ? 64.0f : 40.0f; + const ImVec2 start = ImGui::GetCursorScreenPos(); + + ImGui::InvisibleButton(id, ImVec2(width, height)); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + const ImVec2 min = ImGui::GetItemRectMin(); + const ImVec2 max = ImGui::GetItemRectMax(); + const bool active = ImGui::IsItemActive(); + const bool hovered = ImGui::IsItemHovered(); + const ImU32 button_color = ImGui::GetColorU32( + active ? ImGuiCol_ButtonActive : hovered ? ImGuiCol_ButtonHovered : ImGuiCol_Button); + + draw_list->AddRectFilled(min, max, button_color, ImGui::GetStyle().FrameRounding); + draw_list->AddRect(min, max, ImGui::GetColorU32(ImGuiCol_Border), ImGui::GetStyle().FrameRounding); + + const ImVec2 swatch_min(start.x + 6.0f, start.y + (height - kButtonSwatchHeight) * 0.5f); + const ImVec2 swatch_max(swatch_min.x + kButtonSwatchWidth, swatch_min.y + kButtonSwatchHeight); + drawSwatch(*draw_list, swatch_min, swatch_max, color, false, false); + + if (show_index) { + char label[8] = {}; + std::snprintf(label, sizeof(label), "%u", static_cast(color)); + const ImVec2 text_size = ImGui::CalcTextSize(label); + const ImVec2 text_pos(swatch_max.x + 6.0f, start.y + (height - text_size.y) * 0.5f); + draw_list->AddText(text_pos, ImGui::GetColorU32(ImGuiCol_Text), label); + } +} + +} // namespace + +bool LightColorPalettePicker(const char* id, uint8_t& color, bool show_index, const char* tooltip) { + bool changed = false; + if (color > Config::Lighting::MAX_SERVER_LIGHT_COLOR) { + color = Config::Lighting::MAX_SERVER_LIGHT_COLOR; + changed = true; + } + + ImGui::PushID(id); + drawCurrentColorButton("button", color, show_index); + if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { + ImGui::OpenPopup("light_color_palette"); + } + if (ImGui::IsItemHovered() && tooltip) { + ImGui::SetTooltip("%s: %u", tooltip, static_cast(color)); + } + + if (ImGui::BeginPopup("light_color_palette")) { + if (tooltip) { + ImGui::TextUnformatted(tooltip); + ImGui::Separator(); + } + + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(kCellPadding, kCellPadding)); + for (int index = 0; index < Config::Lighting::LIGHT_COLOR_COUNT; ++index) { + ImGui::PushID(index); + const ImVec2 swatch_min = ImGui::GetCursorScreenPos(); + ImGui::InvisibleButton("swatch", ImVec2(kCellSize, kCellSize)); + const bool clicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); + const bool hovered = ImGui::IsItemHovered(); + const auto palette_index = static_cast(index); + drawSwatch(*ImGui::GetWindowDrawList(), + swatch_min, + ImVec2(swatch_min.x + kCellSize, swatch_min.y + kCellSize), + palette_index, + color == palette_index, + hovered); + + if (hovered) { + uint8_t r = 0; + uint8_t g = 0; + uint8_t b = 0; + Rendering::LightColorPalette::from8bit(palette_index, r, g, b); + ImGui::SetTooltip("Index %d | RGB(%u, %u, %u)", + index, + static_cast(r), + static_cast(g), + static_cast(b)); + } + + if (clicked) { + color = palette_index; + changed = true; + ImGui::CloseCurrentPopup(); + } + + ImGui::PopID(); + if ((index + 1) % kPaletteColumns != 0) { + ImGui::SameLine(); + } + } + ImGui::PopStyleVar(); + + ImGui::EndPopup(); + } + + ImGui::PopID(); + return changed; +} + +} // namespace MapEditor::UI diff --git a/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h b/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h new file mode 100644 index 0000000..f40bdf6 --- /dev/null +++ b/ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h @@ -0,0 +1,12 @@ +#pragma once + +#include + +namespace MapEditor::UI { + +bool LightColorPalettePicker(const char* id, + uint8_t& color, + bool show_index = true, + const char* tooltip = "Server Light Color"); + +} // namespace MapEditor::UI diff --git a/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp b/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp index 21d641b..7efd486 100644 --- a/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp +++ b/ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp @@ -11,6 +11,7 @@ #include "Rendering/Map/MapRenderer.h" #include "Rendering/Passes/IngamePreviewRenderer.h" #include "Services/ViewSettings.h" +#include "UI/Widgets/LightColorPalettePicker.h" namespace MapEditor { namespace UI { @@ -74,10 +75,27 @@ void IngameBoxWindow::render(Domain::ChunkedMap* map, // Ambient slider (only when lighting is enabled) if (settings.preview_lighting_enabled) { ImGui::SameLine(); - ImGui::SetNextItemWidth(80); + ImGui::SetNextItemWidth(70); + int intensity = static_cast(settings.preview_server_light_intensity); + if (ImGui::SliderInt("##intensity", &intensity, 0, 255)) { + settings.preview_server_light_intensity = static_cast(intensity); + } + if (ImGui::IsItemHovered()) { + ImGui::SetTooltip("Server Light Intensity"); + } + ImGui::SameLine(); + uint8_t color = settings.preview_server_light_color; + if (LightColorPalettePicker("##previewServerLightColor", + color, + false, + "Server Color: world light color from the Tibia 8-bit palette")) { + settings.preview_server_light_color = color; + } + ImGui::SameLine(); + ImGui::SetNextItemWidth(70); ImGui::SliderInt("##ambient", &settings.preview_ambient_light, 0, 255); if (ImGui::IsItemHovered()) { - ImGui::SetTooltip("Ambient Light Level"); + ImGui::SetTooltip("Min Ambient"); } }