From 1c9feaac46a6128b4a1dd4807fe9dd7bc61d29f0 Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Thu, 4 Jun 2026 15:35:26 +0200 Subject: [PATCH 1/6] refactor(rendering): align corpse/mount drawing with OTClient MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented multi-tile corpse rendering before creatures and aligned mount drawing logic with OTClient parity. Added support for ping-pong animations and per-instance random phase offsets. - `ImguiMapEditor/Domain/ItemType.h` — added `is_lying_object` flag - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — added ping-pong phase logic, handled `start_frame` and `is_async` offsets - `ImguiMapEditor/Rendering/Animation/ItemAnimation.h` — declared `getPingPongPhase` - `ImguiMapEditor/Rendering/Map/TileRenderer.cpp` — added pre-creature drawing for multi-tile lying objects - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — extracted `drawMount` and `emitPlaceholder` methods, updated drawing order - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.h` — added helper methods for mount and placeholder rendering - `ImguiMapEditor/Rendering/Tile/ItemRenderer.h` — updated common item rendering to reverse order and exclude multi-tile lying corpses - `ImguiMapEditor/Services/ClientDataService.cpp` — mapped `is_lying_object` from DAT data --- ImguiMapEditor/Domain/ItemType.h | 3 + .../Rendering/Animation/ItemAnimation.cpp | 53 +++- .../Rendering/Animation/ItemAnimation.h | 1 + ImguiMapEditor/Rendering/Map/TileRenderer.cpp | 16 +- .../Rendering/Tile/CreatureRenderer.cpp | 248 ++++++++++-------- .../Rendering/Tile/CreatureRenderer.h | 7 + ImguiMapEditor/Rendering/Tile/ItemRenderer.h | 29 +- ImguiMapEditor/Services/ClientDataService.cpp | 3 + 8 files changed, 232 insertions(+), 128 deletions(-) diff --git a/ImguiMapEditor/Domain/ItemType.h b/ImguiMapEditor/Domain/ItemType.h index b9b4cfb..b9ebf4b 100644 --- a/ImguiMapEditor/Domain/ItemType.h +++ b/ImguiMapEditor/Domain/ItemType.h @@ -251,6 +251,9 @@ class ItemType { bool is_wall = false; bool is_locked = false; // Door key lock status (for highlight_locked_doors) + // Lying object (multi-tile corpses) — drawn before creatures + bool is_lying_object = false; + // Sprite IDs for rendering std::vector sprite_ids; diff --git a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp index fbdcdd9..4419b93 100644 --- a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp +++ b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp @@ -25,23 +25,62 @@ int ItemAnimation::getPhaseFromFrames(int frames, int64_t global_ms, return tick % frames; } +int ItemAnimation::getPingPongPhase(int raw_phase, int frames) { + if (frames <= 1) return 0; + int cycle = frames * 2 - 2; + if (cycle <= 0) return 0; + int mod = raw_phase % cycle; + return mod >= frames ? cycle - mod : mod; +} + int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, int tile_x, int tile_y, int tile_z) { int frames = std::max(item.frames, 1); if (frames <= 1) return 0; - bool per_instance = !item.frame_durations.empty() && item.animation_mode == 0; - int tile_offset = per_instance ? (tile_x * 17 + tile_y * 31 + tile_z * 7) : 0; + // animation_mode: 0=async (per-tile offset), 1=sync (global clock) + bool is_async = !item.frame_durations.empty() && item.animation_mode == 0; + int tile_offset = is_async ? (tile_x * 17 + tile_y * 31 + tile_z * 7) : 0; + + // start_frame: 255=random per instance (async only), >0=fixed phase + int start_phase = 0; + if (item.start_frame == 255 && is_async) { + start_phase = (tile_x * 17 + tile_y * 31 + tile_z * 7) % frames; + } else if (item.start_frame > 0) { + start_phase = item.start_frame % frames; + } + // Ping-pong (loop_count == -1) + if (item.loop_count == -1) { + if (item.frame_durations.empty()) { + int tick = static_cast(global_ms / 500); + return getPingPongPhase(tick + tile_offset + start_phase, frames); + } + int total = static_cast(item.total_duration); + if (total <= 0) return start_phase; + int cycle_total = total * 2; + int elapsed = static_cast((global_ms + static_cast(tile_offset) * 500) % cycle_total); + if (elapsed >= total) + elapsed = cycle_total - elapsed; + for (int phase = 0; phase < static_cast(item.frame_durations.size()); ++phase) { + int dur = getPhaseDuration(item, phase); + if (elapsed < dur) + return (phase + start_phase) % frames; + elapsed -= dur; + } + return start_phase; + } + + // Forward loop (loop_count == 0 or > 0) if (item.frame_durations.empty()) { int tick = static_cast(global_ms / 500); - return (tick + tile_offset) % frames; + return (tick + tile_offset + start_phase) % frames; } int total = static_cast(item.total_duration); if (total <= 0) - return 0; + return start_phase; int64_t elapsed_ms = global_ms + static_cast(tile_offset) * 500; int elapsed = static_cast(elapsed_ms % total); @@ -49,18 +88,18 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, for (int phase = 0; phase < static_cast(item.frame_durations.size()); ++phase) { int dur = getPhaseDuration(item, phase); if (elapsed < dur) - return phase % frames; + return (phase + start_phase) % frames; elapsed -= dur; } - return 0; + return start_phase; } int ItemAnimation::getPhaseDuration(const Domain::ItemType &item, int phase) { if (phase < 0 || phase >= static_cast(item.frame_durations.size())) return 500; const auto &d = item.frame_durations[phase]; - return (d.first + d.second) / 2; + return static_cast((d.first + d.second) / 2); } } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Animation/ItemAnimation.h b/ImguiMapEditor/Rendering/Animation/ItemAnimation.h index bcdf276..c77cc54 100644 --- a/ImguiMapEditor/Rendering/Animation/ItemAnimation.h +++ b/ImguiMapEditor/Rendering/Animation/ItemAnimation.h @@ -16,6 +16,7 @@ struct ItemAnimation { private: static int getPhaseDuration(const Domain::ItemType &item, int phase); + static int getPingPongPhase(int raw_phase, int frames); }; } // namespace Rendering diff --git a/ImguiMapEditor/Rendering/Map/TileRenderer.cpp b/ImguiMapEditor/Rendering/Map/TileRenderer.cpp index 54a6b90..c688f88 100644 --- a/ImguiMapEditor/Rendering/Map/TileRenderer.cpp +++ b/ImguiMapEditor/Rendering/Map/TileRenderer.cpp @@ -189,15 +189,25 @@ void TileRenderer::queueTile(const Domain::Tile &tile, int tile_x, int tile_y, } } - // Queue all items - delegate to ItemRenderer - // Pass cached items with pre-resolved types + // Queue all items item_renderer_.queueAll(render_cache_, screen_x, screen_y, size, tile_x, tile_y, tile_z, anim_ticks, ground_color, item_alpha, isItemSelected, view_settings_, missing_sprites, &accumulated_elevation, tile_has_hook_south, tile_has_hook_east, check_tooltips, &tile_needs_tooltip); - // CREATURE RENDERING - Per-tile immediate (OTClient parity) + // Corpse correction: draw multi-tile lying corpses before creatures + for (const auto &[item, type] : render_cache_) { + if (type && type->is_lying_object && (type->width > 1 || type->height > 1)) { + item_renderer_.queueWithColor( + type, screen_x, screen_y, size, tile_x, tile_y, tile_z, anim_ticks, + missing_sprites, ground_color.r, ground_color.g, ground_color.b, + item_alpha, &accumulated_elevation, item, 0, + tile_has_hook_south, tile_has_hook_east); + } + } + + // Creature rendering (per-tile for correct isometric depth) // With diagonal tile iteration, creatures must be rendered PER-TILE to // maintain correct isometric depth. NW tiles (drawn first) should have // creatures that can be overlapped by SE tile items (drawn later). Deferring diff --git a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp index f703143..245cd14 100644 --- a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp +++ b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp @@ -19,67 +19,36 @@ void CreatureRenderer::queue(const Domain::Creature *creature, float screen_x, const TileColor &ground_color, float alpha, uint8_t direction, int animation_frame, std::vector &missing_sprites) { - if (!creature || !client_data_) { + if (!creature || !client_data_) return; - } - // Look up creature type to get outfit const Domain::CreatureType *creature_type = client_data_->getCreatureType(creature->name); if (!creature_type || creature_type->outfit.lookType == 0) { - // No creature type found - render magenta placeholder - const AtlasRegion *white_region = - sprite_manager_.getAtlasManager().getWhitePixel(); - if (white_region) { - emitter_.emit(screen_x, screen_y, size, size, *white_region, - Config::Colors::INVALID_CREATURE_R, - Config::Colors::INVALID_CREATURE_G, - Config::Colors::INVALID_CREATURE_B, alpha * 0.7f); - } + emitPlaceholder(screen_x, screen_y, size, alpha); return; } const Domain::Outfit &outfit = creature_type->outfit; - - // Get outfit data from client const IO::ClientItem *outfit_data = client_data_->getOutfitData(outfit.lookType); if (!outfit_data || outfit_data->sprite_ids.empty()) { - // No outfit data - render magenta placeholder - const AtlasRegion *white_region = - sprite_manager_.getAtlasManager().getWhitePixel(); - if (white_region) { - emitter_.emit(screen_x, screen_y, size, size, *white_region, - Config::Colors::INVALID_CREATURE_R, - Config::Colors::INVALID_CREATURE_G, - Config::Colors::INVALID_CREATURE_B, alpha * 0.7f); - } + emitPlaceholder(screen_x, screen_y, size, alpha); return; } - // Calculate color modulation (lighting + selection) TileColor creature_color = ground_color; - - // Apply selection dimming if (creature->isSelected()) { creature_color.r *= 0.5f; creature_color.g *= 0.5f; creature_color.b *= 0.5f; } - const int width = std::max(1, outfit_data->width); - const int height = std::max(1, outfit_data->height); - const int layers = std::max(1, outfit_data->layers); const int pattern_x = std::max(1, outfit_data->pattern_x); + int dir = direction < pattern_x ? direction : 2 % pattern_x; - // Map direction to pattern_x - int dir = 2 % pattern_x; // Default to South - if (direction < pattern_x) { - dir = direction; - } - - // Choose sprite source based on frame groups and walking state - const std::vector* sprites = &outfit_data->sprite_ids; + // Select sprite source and animation frame + const std::vector *sprites = &outfit_data->sprite_ids; int frame = 0; if (outfit_data->has_frame_groups && animation_frame == 0) { if (!outfit_data->idle_sprite_ids.empty()) { @@ -96,92 +65,165 @@ void CreatureRenderer::queue(const Domain::Creature *creature, float screen_x, } } else if (animation_frame > 0 && !outfit_data->walk_sprite_ids.empty()) { sprites = &outfit_data->walk_sprite_ids; - int walk_frames = std::max(1, outfit_data->walk_frames); - frame = animation_frame % walk_frames; + frame = animation_frame % std::max(1, outfit_data->walk_frames); } else { int f = std::max(1, outfit_data->frames); frame = (f > 1) ? (animation_frame % f) : 0; } - // For each tile part of a multi-tile creature - // RME formula: draw at (screenx - cx * TileSize, screeny - cy * TileSize) - // cx=0,cy=0 is at anchor (screenx, screeny) - // cx increases leftward, cy increases upward - for (int cy = 0; cy < height; ++cy) { - for (int cx = 0; cx < width; ++cx) { - // Get the base sprite index - // RME uses: getHardwareID(cx, cy, dir, pattern_y, pattern_z, outfit, - // frame) Which maps to getSpriteIndex(w, h, layer, pattern_x, pattern_y, - // pattern_z, frame) - uint32_t base_sprite_idx = Utils::SpriteUtils::getSpriteIndex( - outfit_data, cx, cy, // width/height position (NOT reversed!) - 0, // layer 0 = base - dir, // pattern_x = direction - 0, // pattern_y = addon (0 = base outfit) - 0, // pattern_z = mount (0 = no mount) - frame); - - if (base_sprite_idx >= sprites->size()) { - continue; + // Draw mount before rider + int z_pattern = 0; + if (outfit.lookMount > 0) { + z_pattern = 1; + drawMount(outfit, dir, animation_frame, screen_x, screen_y, size, + creature_color, alpha, anim_ticks, missing_sprites); + } + + // Draw outfit + addons (yPattern iteration) + const int width = std::max(1, outfit_data->width); + const int height = std::max(1, outfit_data->height); + const int layers = std::max(1, outfit_data->layers); + const int pattern_y_count = std::max(1, outfit_data->pattern_y); + uint16_t addons = outfit.lookAddons; + + for (int yPattern = 0; yPattern < pattern_y_count; ++yPattern) { + if (yPattern > 0 && !(addons & (1 << (yPattern - 1)))) + continue; + + for (int cy = 0; cy < height; ++cy) { + for (int cx = 0; cx < width; ++cx) { + uint32_t base_idx = Utils::SpriteUtils::getSpriteIndex( + outfit_data, cx, cy, 0, dir, yPattern, z_pattern, frame); + if (base_idx >= sprites->size()) + continue; + + uint32_t base_id = (*sprites)[base_idx]; + if (base_id == 0) + continue; + + uint32_t template_id = 0; + if (layers >= 2) { + uint32_t t_idx = Utils::SpriteUtils::getSpriteIndex( + outfit_data, cx, cy, 1, dir, yPattern, z_pattern, frame); + if (t_idx < sprites->size()) + template_id = (*sprites)[t_idx]; + } + + const AtlasRegion *region = + sprite_manager_.getCreatureSpriteService().getColorizedOutfitRegion( + base_id, template_id, + static_cast(outfit.lookHead), + static_cast(outfit.lookBody), + static_cast(outfit.lookLegs), + static_cast(outfit.lookFeet)); + if (!region) { + missing_sprites.push_back(base_id); + if (template_id != 0) + missing_sprites.push_back(template_id); + continue; + } + + float scale = size / TILE_SIZE; + float dx = screen_x - static_cast(cx) * size - + (outfit_data->has_offset ? outfit_data->offset_x : 0) * scale; + float dy = screen_y - static_cast(cy) * size - + (outfit_data->has_offset ? outfit_data->offset_y : 0) * scale; + + emitter_.emit(dx, dy, size, size, *region, creature_color.r, + creature_color.g, creature_color.b, alpha); } + } + } +} + +void CreatureRenderer::drawMount(const Domain::Outfit &outfit, int dir, + int animation_frame, float screen_x, + float screen_y, float size, + const TileColor &color, float alpha, + const AnimationTicks &anim_ticks, + std::vector &missing_sprites) { + const IO::ClientItem *mount_data = + client_data_->getOutfitData(outfit.lookMount); + if (!mount_data || mount_data->sprite_ids.empty()) + return; + + const std::vector *mount_sprites = &mount_data->sprite_ids; + int mount_dir = dir % std::max(1, mount_data->pattern_x); + int mount_frame = 0; + + if (animation_frame > 0 && !mount_data->walk_sprite_ids.empty()) { + mount_sprites = &mount_data->walk_sprite_ids; + mount_frame = animation_frame % std::max(1, mount_data->walk_frames); + } else if (mount_data->has_frame_groups && !mount_data->idle_sprite_ids.empty()) { + mount_sprites = &mount_data->idle_sprite_ids; + mount_frame = ItemAnimation::getPhaseFromFrames( + static_cast(mount_data->idle_frames), anim_ticks.global_ms, + mount_data->has_animation_data ? &mount_data->frame_durations : nullptr, + mount_data->total_duration); + } else if (mount_data->frames > 1) { + mount_frame = ItemAnimation::getPhaseFromFrames( + static_cast(mount_data->frames), anim_ticks.global_ms, + mount_data->has_animation_data ? &mount_data->frame_durations : nullptr, + mount_data->total_duration); + } + + int mw = std::max(1, mount_data->width); + int mh = std::max(1, mount_data->height); + int ml = std::max(1, mount_data->layers); + float scale = size / TILE_SIZE; - uint32_t base_sprite_id = (*sprites)[base_sprite_idx]; - if (base_sprite_id == 0) { + for (int cy = 0; cy < mh; ++cy) { + for (int cx = 0; cx < mw; ++cx) { + uint32_t base_idx = Utils::SpriteUtils::getSpriteIndex( + mount_data, cx, cy, 0, mount_dir, 0, 0, mount_frame); + if (base_idx >= mount_sprites->size()) + continue; + uint32_t base_id = (*mount_sprites)[base_idx]; + if (base_id == 0) continue; - } - // Get template sprite for colorization - uint32_t template_sprite_id = 0; - if (layers >= 2) { - uint32_t template_sprite_idx = Utils::SpriteUtils::getSpriteIndex( - outfit_data, cx, cy, 1, dir, 0, 0, frame); - if (template_sprite_idx < sprites->size()) { - template_sprite_id = (*sprites)[template_sprite_idx]; - } + uint32_t template_id = 0; + if (ml >= 2) { + uint32_t t_idx = Utils::SpriteUtils::getSpriteIndex( + mount_data, cx, cy, 1, mount_dir, 0, 0, mount_frame); + if (t_idx < mount_sprites->size()) + template_id = (*mount_sprites)[t_idx]; } - // Get the colorized outfit sprite region from the atlas const AtlasRegion *region = sprite_manager_.getCreatureSpriteService().getColorizedOutfitRegion( - base_sprite_id, template_sprite_id, - static_cast(outfit.lookHead), - static_cast(outfit.lookBody), - static_cast(outfit.lookLegs), - static_cast(outfit.lookFeet)); - + base_id, template_id, + static_cast(outfit.lookMountHead), + static_cast(outfit.lookMountBody), + static_cast(outfit.lookMountLegs), + static_cast(outfit.lookMountFeet)); if (!region) { - // Queue for async load - missing_sprites.push_back(base_sprite_id); - if (template_sprite_id != 0) { - missing_sprites.push_back(template_sprite_id); - } + missing_sprites.push_back(base_id); + if (template_id != 0) + missing_sprites.push_back(template_id); continue; } - // Calculate draw position - SIMPLE RME FORMULA + DISPLACEMENT - // screenx - cx * TileSize - offsetX, screeny - cy * TileSize - offsetY - // The displacement offset centers creatures on the tile (OTClient - // parity). This puts cx=0,cy=0 at anchor, and expands left/up for larger - // creatures - float displacement_x = outfit_data->has_offset - ? static_cast(outfit_data->offset_x) - : 0.0f; - float displacement_y = outfit_data->has_offset - ? static_cast(outfit_data->offset_y) - : 0.0f; - // Scale displacement by (size / TILE_SIZE) for zoom - float scale = size / TILE_SIZE; - float draw_x = - screen_x - static_cast(cx) * size - displacement_x * scale; - float draw_y = - screen_y - static_cast(cy) * size - displacement_y * scale; - - // Queue to sprite batch - emitter_.emit(draw_x, draw_y, size, size, *region, creature_color.r, - creature_color.g, creature_color.b, alpha); + float dx = screen_x - static_cast(cx) * size - + (mount_data->has_offset ? mount_data->offset_x : 0) * scale; + float dy = screen_y - static_cast(cy) * size - + (mount_data->has_offset ? mount_data->offset_y : 0) * scale; + + emitter_.emit(dx, dy, size, size, *region, color.r, color.g, color.b, alpha); } } } +void CreatureRenderer::emitPlaceholder(float screen_x, float screen_y, + float size, float alpha) { + const AtlasRegion *white = sprite_manager_.getAtlasManager().getWhitePixel(); + if (white) { + emitter_.emit(screen_x, screen_y, size, size, *white, + Config::Colors::INVALID_CREATURE_R, + Config::Colors::INVALID_CREATURE_G, + Config::Colors::INVALID_CREATURE_B, alpha * 0.7f); + } +} + } // namespace Rendering } // namespace MapEditor diff --git a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.h b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.h index a718712..22331c3 100644 --- a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.h +++ b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.h @@ -61,6 +61,13 @@ class CreatureRenderer { std::vector& missing_sprites); private: + void drawMount(const Domain::Outfit &outfit, int dir, int animation_frame, + float screen_x, float screen_y, float size, + const TileColor &color, float alpha, + const AnimationTicks &anim_ticks, + std::vector &missing_sprites); + void emitPlaceholder(float screen_x, float screen_y, float size, float alpha); + SpriteEmitter& emitter_; Services::SpriteManager& sprite_manager_; const Services::ClientDataService* client_data_; diff --git a/ImguiMapEditor/Rendering/Tile/ItemRenderer.h b/ImguiMapEditor/Rendering/Tile/ItemRenderer.h index d5305e5..ee79e99 100644 --- a/ImguiMapEditor/Rendering/Tile/ItemRenderer.h +++ b/ImguiMapEditor/Rendering/Tile/ItemRenderer.h @@ -195,29 +195,28 @@ void ItemRenderer::queueAll( // ============================================================ // MULTI-PASS RENDERING (OTClient Painter Algorithm) - // ============================================================ - // Pass 1: OnBottom items (walls, pillars) - FORWARD order - // Pass 2: Common items (furniture, decorations) - REVERSE order - // NOTE: OnTop items (P3) are rendered immediately per-tile in - // TileRenderer::queueTile() AFTER creatures for correct depth. + // Pass 1: OnBottom (walls, pillars) — forward + // Pass 2: Common items — reverse (OTClient rbegin→rend) + // OnTop items rendered after creatures in TileRenderer // ============================================================ - // PASS 1: OnBottom items (priority 2) - FORWARD + // PASS 1: OnBottom items — forward for (const auto &entry : items) { if (entry.type && entry.type->is_on_bottom) { renderItem(entry); } } - // PASS 2: Common items (priority 5) - FORWARD (like RME) - // Items at the end of the vector (topmost) must be drawn LAST to appear on top - for (const auto &entry : items) { - if (entry.type && !entry.type->is_on_bottom && !entry.type->is_on_top) { - renderItem(entry); - } else if (!entry.type) { - // Items without type info are treated as common items - renderItem(entry); - } + // PASS 2: Common items — reverse (OTClient parity) + // Multi-tile lying corpses excluded (drawn pre-creature in TileRenderer) + for (auto it = items.rbegin(); it != items.rend(); ++it) { + const auto &entry = *it; + if (entry.type && entry.type->is_on_bottom) continue; + if (entry.type && entry.type->is_on_top) continue; + if (entry.type && entry.type->is_lying_object && + (entry.type->width > 1 || entry.type->height > 1)) + continue; + renderItem(entry); } // NOTE: OnTop items rendered per-tile in TileRenderer::queueTile() diff --git a/ImguiMapEditor/Services/ClientDataService.cpp b/ImguiMapEditor/Services/ClientDataService.cpp index 92ac636..5ccfc72 100644 --- a/ImguiMapEditor/Services/ClientDataService.cpp +++ b/ImguiMapEditor/Services/ClientDataService.cpp @@ -401,6 +401,9 @@ void ClientDataService::mergeOtbWithDat( merged.total_duration += (d.first + d.second) / 2; } + // Lying object (multi-tile corpses) + merged.is_lying_object = dat->is_lying_object; + // Frame groups (10.57+ creatures) merged.idle_sprite_ids = dat->idle_sprite_ids; merged.walk_sprite_ids = dat->walk_sprite_ids; From 365f56d1588f77598b2f71d3d0d45578ed7a78dd Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Thu, 4 Jun 2026 16:10:24 +0200 Subject: [PATCH 2/6] refactor(ui): introduce uniform preview card rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Standardized item and brush preview rendering into a unified `RenderPreviewCard` utility to enforce consistent styling, selection feedback, and rounded aesthetics. Updated compositor logic to correctly support multi-layer items by iterating through layers before grid tiles. - `ImguiMapEditor/Services/ItemCompositor.cpp` — updated composite logic to iterate layers first, matching map renderer order; removed hardcoded background color - `ImguiMapEditor/UI/Utils/PreviewUtils.cpp` — implemented `RenderPreviewCard` for uniform rounded UI element rendering with state support - `ImguiMapEditor/UI/Utils/PreviewUtils.hpp` — added `RenderPreviewCard` declaration - `ImguiMapEditor/UI/Widgets/Properties/PropertyPanelRenderer.cpp` — migrated slot rendering to `RenderPreviewCard` - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp` — migrated brush grid to `RenderPreviewCard`, cleaned up redundant drawing logic - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.h` — updated `getBrushTexture` return type to `Rendering::Texture*` - `ImguiMapEditor/UI/Widgets/TilesetWidget.cpp` — migrated item grid to `RenderPreviewCard` - `ImguiMapEditor/UI/Windows/BrowseTile/ItemsListRenderer.cpp` — migrated item list row rendering to `RenderPreviewCard` --- ImguiMapEditor/Services/ItemCompositor.cpp | 72 ++++++--------- ImguiMapEditor/UI/Utils/PreviewUtils.cpp | 33 ++++++- ImguiMapEditor/UI/Utils/PreviewUtils.hpp | 8 ++ .../Properties/PropertyPanelRenderer.cpp | 6 +- .../UI/Widgets/TilesetGridWidget.cpp | 90 ++++++------------- ImguiMapEditor/UI/Widgets/TilesetGridWidget.h | 8 +- ImguiMapEditor/UI/Widgets/TilesetWidget.cpp | 3 +- .../Windows/BrowseTile/ItemsListRenderer.cpp | 6 +- 8 files changed, 113 insertions(+), 113 deletions(-) diff --git a/ImguiMapEditor/Services/ItemCompositor.cpp b/ImguiMapEditor/Services/ItemCompositor.cpp index f8173a5..d295e42 100644 --- a/ImguiMapEditor/Services/ItemCompositor.cpp +++ b/ImguiMapEditor/Services/ItemCompositor.cpp @@ -19,22 +19,19 @@ ItemCompositor::getCompositedItemTexture(const Domain::ItemType *type) { const uint16_t client_id = type->client_id; - // Check cache first auto it = cache_.find(client_id); if (it != cache_.end()) { return it->second.get(); } - // For single-tile items (1x1), load directly and cache - if (type->width == 1 && type->height == 1) { + // Single-tile single-layer: load directly + if (type->width == 1 && type->height == 1 && type->layers <= 1) { uint32_t sprite_id = type->sprite_ids[0]; auto sprite_data = Utils::SpriteUtils::loadDecodedSprite(spr_reader_, sprite_id); if (sprite_data.empty()) { return nullptr; } - - // Create and cache texture auto texture = std::make_unique(32, 32, sprite_data.data()); Rendering::Texture *ptr = texture.get(); @@ -42,51 +39,40 @@ ItemCompositor::getCompositedItemTexture(const Domain::ItemType *type) { return ptr; } - // Multi-tile item: need to composite all parts - const uint8_t width = type->width; - const uint8_t height = type->height; - const int composite_size = std::max(width, height) * 32; + // Multi-tile or multi-layer: composite all parts + const int w = std::max(1, type->width); + const int h = std::max(1, type->height); + const int layers = std::max(1, type->layers); + const int composite_size = std::max(w, h) * 32; - // Create RGBA buffer for the composited image std::vector composite_rgba(composite_size * composite_size * 4, 0); - // Background color (same as RME's ICON_BACKGROUND setting - gray) - constexpr uint8_t BG_SHADE = 48; - for (size_t i = 0; i < composite_rgba.size(); i += 4) { - composite_rgba[i + 0] = BG_SHADE; - composite_rgba[i + 1] = BG_SHADE; - composite_rgba[i + 2] = BG_SHADE; - composite_rgba[i + 3] = 255; - } - - // Composite each sprite part - for (uint8_t h = 0; h < height; ++h) { - for (uint8_t w = 0; w < width; ++w) { - size_t sprite_index = static_cast(h) * width + w; - - if (sprite_index >= type->sprite_ids.size()) { - continue; + // Iterate layers, then tile grid (matches map renderer order) + for (int layer = 0; layer < layers; ++layer) { + for (int cy = 0; cy < h; ++cy) { + for (int cx = 0; cx < w; ++cx) { + size_t sprite_index = + (static_cast(layer) * h + cy) * w + cx; + if (sprite_index >= type->sprite_ids.size()) + continue; + + uint32_t sprite_id = type->sprite_ids[sprite_index]; + auto sprite_data = + Utils::SpriteUtils::loadDecodedSprite(spr_reader_, sprite_id); + if (sprite_data.size() < 32 * 32 * 4) + continue; + + // Reversed position (matches OTClient/RME: anchor at bottom-right) + int dest_x = (w - cx - 1) * 32; + int dest_y = (h - cy - 1) * 32; + + Utils::ImageBlending::blendSpriteTile(sprite_data.data(), + composite_rgba.data(), + composite_size, dest_x, dest_y); } - - uint32_t sprite_id = type->sprite_ids[sprite_index]; - auto sprite_data = - Utils::SpriteUtils::loadDecodedSprite(spr_reader_, sprite_id); - if (sprite_data.size() < 32 * 32 * 4) { - continue; - } - - // Calculate destination position (same as RME: width-w-1, height-h-1) - int dest_x = (width - w - 1) * 32; - int dest_y = (height - h - 1) * 32; - - // Blend sprite tile onto composite canvas - Utils::ImageBlending::blendSpriteTile(sprite_data.data(), - composite_rgba.data(), - composite_size, dest_x, dest_y); } } - // Create texture at full composite resolution auto texture = std::make_unique( composite_size, composite_size, composite_rgba.data()); Rendering::Texture *ptr = texture.get(); diff --git a/ImguiMapEditor/UI/Utils/PreviewUtils.cpp b/ImguiMapEditor/UI/Utils/PreviewUtils.cpp index b02510e..63c2893 100644 --- a/ImguiMapEditor/UI/Utils/PreviewUtils.cpp +++ b/ImguiMapEditor/UI/Utils/PreviewUtils.cpp @@ -3,6 +3,7 @@ #include "Services/ClientDataService.h" #include "Rendering/Tile/CreatureSpriteHelper.h" #include "Rendering/Core/Texture.h" +#include namespace MapEditor::UI::Utils { @@ -41,8 +42,38 @@ CreaturePreviewResult GetCreaturePreview(Services::ClientDataService& clientData CreaturePreviewResult GetCreaturePreview(Services::ClientDataService& clientData, Services::SpriteManager& spriteManager, const Domain::Outfit& outfit) { - // Basic validity check for outfit could go here, but helper handles it. return GetCreaturePreviewImpl(clientData, spriteManager, outfit); } +bool RenderPreviewCard(Rendering::Texture* texture, float size, + bool is_selected, float rounding, float padding) { + ImDrawList* dl = ImGui::GetWindowDrawList(); + ImVec2 min = ImGui::GetCursorScreenPos(); + ImVec2 max(min.x + size, min.y + size); + + ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_FrameBg); + ImU32 border_col = ImGui::GetColorU32(ImGuiCol_Border); + + if (is_selected) { + bg_col = ImGui::GetColorU32(ImGuiCol_Header); + border_col = IM_COL32(255, 200, 0, 255); // Gold + } else if (ImGui::IsMouseHoveringRect(min, max)) { + bg_col = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + } + + dl->AddRectFilled(min, max, bg_col, rounding); + dl->AddRect(min, max, border_col, rounding); + + if (texture) { + ImVec2 img_min(min.x + padding, min.y + padding); + ImVec2 img_max(max.x - padding, max.y - padding); + dl->AddImageRounded( + (void*)(intptr_t)texture->id(), img_min, img_max, + ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, rounding); + } + + ImGui::Dummy(ImVec2(size, size)); + return ImGui::IsItemClicked(); +} + } // namespace MapEditor::UI::Utils diff --git a/ImguiMapEditor/UI/Utils/PreviewUtils.hpp b/ImguiMapEditor/UI/Utils/PreviewUtils.hpp index db682e4..3c29be3 100644 --- a/ImguiMapEditor/UI/Utils/PreviewUtils.hpp +++ b/ImguiMapEditor/UI/Utils/PreviewUtils.hpp @@ -16,6 +16,8 @@ namespace MapEditor::Rendering { class Texture; } // namespace MapEditor::Rendering +struct ImVec2; + namespace MapEditor::UI::Utils { // Retrieves a preview texture for an item. Returns nullptr if not available. @@ -40,4 +42,10 @@ CreaturePreviewResult GetCreaturePreview(Services::ClientDataService& clientData Services::SpriteManager& spriteManager, const Domain::Outfit& outfit); +// Renders a preview texture inside a rounded card with optional selection/hover state. +// Advances the cursor by `size` pixels. Returns true if clicked. +bool RenderPreviewCard(Rendering::Texture* texture, float size, + bool is_selected, float rounding = 4.0f, + float padding = 2.0f); + } // namespace MapEditor::UI::Utils diff --git a/ImguiMapEditor/UI/Widgets/Properties/PropertyPanelRenderer.cpp b/ImguiMapEditor/UI/Widgets/Properties/PropertyPanelRenderer.cpp index 9dcf7fd..51325fe 100644 --- a/ImguiMapEditor/UI/Widgets/Properties/PropertyPanelRenderer.cpp +++ b/ImguiMapEditor/UI/Widgets/Properties/PropertyPanelRenderer.cpp @@ -7,6 +7,7 @@ #include "Domain/ChunkedMap.h" #include "Services/SpriteManager.h" #include "Rendering/Core/Texture.h" +#include "UI/Utils/PreviewUtils.hpp" #include "../../ext/fontawesome6/IconsFontAwesome6.h" #include #include @@ -319,8 +320,9 @@ void PropertyPanelRenderer::renderContainerSection() { if (slot_item && sprite_manager_) { const Domain::ItemType* slot_type = slot_item->getType(); if (slot_type) { - if (auto* tex = sprite_manager_->getItemCompositor().getCompositedItemTexture(slot_type)) { - ImGui::Image((void*)(intptr_t)tex->id(), ImVec2(SLOT_SIZE, SLOT_SIZE)); + if (auto* tex = Utils::GetItemPreview(*sprite_manager_, slot_type)) { + ImGui::SetCursorScreenPos(pos); + Utils::RenderPreviewCard(tex, SLOT_SIZE, false); // Tooltip on hover if (ImGui::IsItemHovered()) { diff --git a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp index ed8e971..bfc1c30 100644 --- a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp +++ b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp @@ -59,7 +59,7 @@ void TilesetGridWidget::setTileset(const std::string &tilesetName) { } } -void *TilesetGridWidget::getBrushTextureId(const Brushes::IBrush *brush) const { +Rendering::Texture *TilesetGridWidget::getBrushTexture(const Brushes::IBrush *brush) const { if (!clientData_ || !spriteManager_ || !brush) { return nullptr; } @@ -69,18 +69,17 @@ void *TilesetGridWidget::getBrushTextureId(const Brushes::IBrush *brush) const { const auto *itemType = clientData_->getItemTypeByServerId( static_cast(rawBrush->getItemId())); if (auto *tex = Utils::GetItemPreview(*spriteManager_, itemType)) { - return reinterpret_cast(static_cast(tex->id())); + return tex; } } - // Try CreatureBrush - use existing utility + // Try CreatureBrush else if (auto *creatureBrush = dynamic_cast(brush)) { const auto &outfit = creatureBrush->getOutfit(); auto preview = Utils::GetCreaturePreview(*clientData_, *spriteManager_, outfit); if (preview.texture) { - return reinterpret_cast( - static_cast(preview.texture->id())); + return preview.texture; } } @@ -286,34 +285,12 @@ void TilesetGridWidget::renderBrushGrid() { ImGui::PushID(static_cast(i)); - // Render brush tile - ImVec2 tileSize(getIconSize(), getIconSize()); - ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - - void *textureId = getBrushTextureId(brush); - - ImGui::InvisibleButton("##tile", tileSize); - bool isHovered = ImGui::IsItemHovered(); - bool isClicked = ImGui::IsItemClicked(); - - ImDrawList *dl = ImGui::GetWindowDrawList(); - - // Background - ImU32 bgColor = - isHovered ? IM_COL32(80, 80, 80, 255) : IM_COL32(40, 40, 40, 255); - dl->AddRectFilled( - cursorPos, ImVec2(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y), - bgColor); - - // Sprite - if (textureId) { - dl->AddImage( - textureId, cursorPos, - ImVec2(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y)); - } + Rendering::Texture *tex = getBrushTexture(brush); + bool isSelected = false; + bool clicked = Utils::RenderPreviewCard(tex, getIconSize(), isSelected); // Tooltip - if (isHovered) { + if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::Text("%s", brush->getName().c_str()); ImGui::TextDisabled("From: %s", bws.sourceTileset.c_str()); @@ -329,7 +306,7 @@ void TilesetGridWidget::renderBrushGrid() { } // Click handling - if (isClicked) { + if (clicked) { selectedBrushName_ = brush->getName(); if (brushController_) { brushController_->setBrush(const_cast(brush)); @@ -411,7 +388,7 @@ void TilesetGridWidget::renderBrushGrid() { ImVec2 tileSize(getIconSize(), getIconSize()); ImVec2 cursorPos = ImGui::GetCursorScreenPos(); - void *textureId = getBrushTextureId(brush); + Rendering::Texture *tex = getBrushTexture(brush); // Drag source ImGui::InvisibleButton("##tile", tileSize); @@ -440,7 +417,6 @@ void TilesetGridWidget::renderBrushGrid() { ImGui::AcceptDragDropPayload("TILESET_ENTRY")) { size_t sourceIdx = *static_cast(payload->Data); if (sourceIdx != fe.originalIndex && tilesetRegistry_) { - // Perform move if (auto *ts = tilesetRegistry_->getTileset(tilesetName_)) { ts->moveEntry(sourceIdx, fe.originalIndex); filterDirty_ = true; @@ -454,23 +430,26 @@ void TilesetGridWidget::renderBrushGrid() { } ImDrawList *dl = ImGui::GetWindowDrawList(); + constexpr float CARD_ROUNDING = 4.0f; + constexpr float IMG_PADDING = 2.0f; + ImVec2 rectMax(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y); - // Background - ImU32 bgColor = IM_COL32(40, 40, 40, 255); + // Rounded card background + ImU32 bgColor = ImGui::GetColorU32(ImGuiCol_FrameBg); if (isSelected) { - bgColor = IM_COL32(60, 100, 160, 255); + bgColor = ImGui::GetColorU32(ImGuiCol_Header); } else if (isHovered) { - bgColor = IM_COL32(80, 80, 80, 255); + bgColor = ImGui::GetColorU32(ImGuiCol_HeaderHovered); } - dl->AddRectFilled( - cursorPos, ImVec2(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y), - bgColor); - - // Sprite - if (textureId) { - dl->AddImage( - textureId, cursorPos, - ImVec2(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y)); + dl->AddRectFilled(cursorPos, rectMax, bgColor, CARD_ROUNDING); + + // Rounded sprite + if (tex) { + ImVec2 img_min(cursorPos.x + IMG_PADDING, cursorPos.y + IMG_PADDING); + ImVec2 img_max(rectMax.x - IMG_PADDING, rectMax.y - IMG_PADDING); + dl->AddImageRounded((void*)(intptr_t)tex->id(), img_min, img_max, + ImVec2(0, 0), ImVec2(1, 1), + IM_COL32_WHITE, CARD_ROUNDING); } // Selection border (with optional pulse animation) @@ -486,30 +465,19 @@ void TilesetGridWidget::renderBrushGrid() { float elapsed = currentTime - pulseStartTime_; if (elapsed < PULSE_DURATION) { - // Pulsing green border float pulse = 0.5f + 0.5f * std::sin(elapsed * 8.0f); ImU32 pulseColor = IM_COL32(static_cast(50 * (1 - pulse)), static_cast(220 * pulse + 35), static_cast(80 * pulse), 255); float thickness = 2.0f + pulse * 2.0f; - dl->AddRect( - cursorPos, - ImVec2(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y), - pulseColor, 0, 0, thickness); + dl->AddRect(cursorPos, rectMax, pulseColor, CARD_ROUNDING, 0, thickness); } else { - // Pulse ended - clear state and show normal border pulseBrushName_.clear(); pulseStartTime_ = -1.0f; - dl->AddRect( - cursorPos, - ImVec2(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y), - IM_COL32(100, 180, 255, 255), 0, 0, 2.0f); + dl->AddRect(cursorPos, rectMax, IM_COL32(100, 180, 255, 255), CARD_ROUNDING, 0, 2.0f); } } else { - dl->AddRect( - cursorPos, - ImVec2(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y), - IM_COL32(100, 180, 255, 255), 0, 0, 2.0f); + dl->AddRect(cursorPos, rectMax, IM_COL32(100, 180, 255, 255), CARD_ROUNDING, 0, 2.0f); } } diff --git a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.h b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.h index d7016a3..b50a91a 100644 --- a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.h +++ b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.h @@ -16,6 +16,10 @@ struct AppSettings; namespace MapEditor { +namespace Rendering { +class Texture; +} + namespace Services { class ClientDataService; class SpriteManager; @@ -173,10 +177,10 @@ class TilesetGridWidget { void applyFilter(); /** - * Get OpenGL texture ID for a brush (RawBrush or CreatureBrush). + * Get preview texture for a brush (RawBrush or CreatureBrush). * Uses PreviewUtils for consistent texture retrieval. */ - void *getBrushTextureId(const Brushes::IBrush *brush) const; + Rendering::Texture *getBrushTexture(const Brushes::IBrush *brush) const; // Services (non-owning) Services::ClientDataService *clientData_ = nullptr; diff --git a/ImguiMapEditor/UI/Widgets/TilesetWidget.cpp b/ImguiMapEditor/UI/Widgets/TilesetWidget.cpp index 4a3fb12..cb908fe 100644 --- a/ImguiMapEditor/UI/Widgets/TilesetWidget.cpp +++ b/ImguiMapEditor/UI/Widgets/TilesetWidget.cpp @@ -266,8 +266,7 @@ void TilesetWidget::renderItemGrid() { if (spriteManager_) { if (auto *texture = Utils::GetItemPreview(*spriteManager_, itemType)) { - clicked = Utils::RenderGridItem((void *)(intptr_t)texture->id(), - iconSize_, isSelected); + clicked = Utils::RenderPreviewCard(texture, iconSize_, isSelected); rendered = true; } } diff --git a/ImguiMapEditor/UI/Windows/BrowseTile/ItemsListRenderer.cpp b/ImguiMapEditor/UI/Windows/BrowseTile/ItemsListRenderer.cpp index 19d46f4..ca9a897 100644 --- a/ImguiMapEditor/UI/Windows/BrowseTile/ItemsListRenderer.cpp +++ b/ImguiMapEditor/UI/Windows/BrowseTile/ItemsListRenderer.cpp @@ -7,6 +7,7 @@ #include "Domain/Tile.h" #include "Rendering/Core/Texture.h" #include "Services/SpriteManager.h" +#include "UI/Utils/PreviewUtils.hpp" #include "../../ext/fontawesome6/IconsFontAwesome6.h" #include @@ -76,8 +77,9 @@ void ItemsListRenderer::renderItemRow(const Domain::Item *item, bool image_rendered = false; if (sprite_manager_ && type) { - if (auto *texture = sprite_manager_->getItemCompositor().getCompositedItemTexture(type)) { - ImGui::Image((void *)(intptr_t)texture->id(), ImVec2(32, 32)); + if (auto *texture = Utils::GetItemPreview(*sprite_manager_, type)) { + bool is_sel = (selected_index == display_index); + Utils::RenderPreviewCard(texture, 32.0f, is_sel); image_rendered = true; } } From 88e19296dafc43ad06aa95664262b344a7b9d68f Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Thu, 4 Jun 2026 16:39:05 +0200 Subject: [PATCH 3/6] refactor(rendering): fix animation and refine rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactored animation and rendering logic to unify sprite index calculation and fix rendering order issues. Consolidated UI brush card rendering and streamlined preview card interactions. - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — fixed ping-pong animation start phase offset and reflection - `ImguiMapEditor/Rendering/Map/TileRenderer.cpp` — removed redundant multi-tile corpse rendering pass - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — extracted mount frame selection into helper - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.h` — added `selectMountFrame` declaration - `ImguiMapEditor/Rendering/Tile/ItemRenderer.h` — updated pass 2 rendering to forward iteration - `ImguiMapEditor/Services/ItemCompositor.cpp` — migrated to `SpriteUtils::getSpriteIndex` - `ImguiMapEditor/UI/Utils/PreviewUtils.cpp` — simplified `RenderPreviewCard` interaction logic - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp` — consolidated card rendering into `renderBrushCard` - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.h` — added `renderBrushCard` declaration - `ImguiMapEditor/Utils/SpriteUtils.cpp` — added `Domain::ItemType` overload for sprite index calculation - `ImguiMapEditor/Utils/SpriteUtils.h` — added `Domain::ItemType` overload declaration and missing header --- .../Rendering/Animation/ItemAnimation.cpp | 18 +-- ImguiMapEditor/Rendering/Map/TileRenderer.cpp | 11 -- .../Rendering/Tile/CreatureRenderer.cpp | 37 ++++-- .../Rendering/Tile/CreatureRenderer.h | 3 + ImguiMapEditor/Rendering/Tile/ItemRenderer.h | 11 +- ImguiMapEditor/Services/ItemCompositor.cpp | 4 +- ImguiMapEditor/UI/Utils/PreviewUtils.cpp | 13 +- .../UI/Widgets/TilesetGridWidget.cpp | 115 +++++++++--------- ImguiMapEditor/UI/Widgets/TilesetGridWidget.h | 8 ++ ImguiMapEditor/Utils/SpriteUtils.cpp | 28 ++++- ImguiMapEditor/Utils/SpriteUtils.h | 23 ++-- 11 files changed, 153 insertions(+), 118 deletions(-) diff --git a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp index 4419b93..8a75b71 100644 --- a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp +++ b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp @@ -28,7 +28,6 @@ int ItemAnimation::getPhaseFromFrames(int frames, int64_t global_ms, int ItemAnimation::getPingPongPhase(int raw_phase, int frames) { if (frames <= 1) return 0; int cycle = frames * 2 - 2; - if (cycle <= 0) return 0; int mod = raw_phase % cycle; return mod >= frames ? cycle - mod : mod; } @@ -39,11 +38,9 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, if (frames <= 1) return 0; - // animation_mode: 0=async (per-tile offset), 1=sync (global clock) bool is_async = !item.frame_durations.empty() && item.animation_mode == 0; int tile_offset = is_async ? (tile_x * 17 + tile_y * 31 + tile_z * 7) : 0; - // start_frame: 255=random per instance (async only), >0=fixed phase int start_phase = 0; if (item.start_frame == 255 && is_async) { start_phase = (tile_x * 17 + tile_y * 31 + tile_z * 7) % frames; @@ -51,8 +48,8 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, start_phase = item.start_frame % frames; } - // Ping-pong (loop_count == -1) if (item.loop_count == -1) { + // Ping-pong: apply start_phase to raw phase BEFORE reflection if (item.frame_durations.empty()) { int tick = static_cast(global_ms / 500); return getPingPongPhase(tick + tile_offset + start_phase, frames); @@ -60,19 +57,22 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, int total = static_cast(item.total_duration); if (total <= 0) return start_phase; int cycle_total = total * 2; - int elapsed = static_cast((global_ms + static_cast(tile_offset) * 500) % cycle_total); + int raw_elapsed = static_cast((global_ms + static_cast(tile_offset) * 500) % cycle_total); + // Apply start_phase offset before reflection + int elapsed = (raw_elapsed + start_phase * (total / std::max(1, frames))) % cycle_total; + // Reflect: map [total, cycle_total) back to [0, total) if (elapsed >= total) - elapsed = cycle_total - elapsed; + elapsed = cycle_total - 1 - elapsed; for (int phase = 0; phase < static_cast(item.frame_durations.size()); ++phase) { int dur = getPhaseDuration(item, phase); if (elapsed < dur) - return (phase + start_phase) % frames; + return phase % frames; elapsed -= dur; } - return start_phase; + return 0; } - // Forward loop (loop_count == 0 or > 0) + // Forward loop if (item.frame_durations.empty()) { int tick = static_cast(global_ms / 500); return (tick + tile_offset + start_phase) % frames; diff --git a/ImguiMapEditor/Rendering/Map/TileRenderer.cpp b/ImguiMapEditor/Rendering/Map/TileRenderer.cpp index c688f88..d86ce6d 100644 --- a/ImguiMapEditor/Rendering/Map/TileRenderer.cpp +++ b/ImguiMapEditor/Rendering/Map/TileRenderer.cpp @@ -196,17 +196,6 @@ void TileRenderer::queueTile(const Domain::Tile &tile, int tile_x, int tile_y, &accumulated_elevation, tile_has_hook_south, tile_has_hook_east, check_tooltips, &tile_needs_tooltip); - // Corpse correction: draw multi-tile lying corpses before creatures - for (const auto &[item, type] : render_cache_) { - if (type && type->is_lying_object && (type->width > 1 || type->height > 1)) { - item_renderer_.queueWithColor( - type, screen_x, screen_y, size, tile_x, tile_y, tile_z, anim_ticks, - missing_sprites, ground_color.r, ground_color.g, ground_color.b, - item_alpha, &accumulated_elevation, item, 0, - tile_has_hook_south, tile_has_hook_east); - } - } - // Creature rendering (per-tile for correct isometric depth) // With diagonal tile iteration, creatures must be rendered PER-TILE to // maintain correct isometric depth. NW tiles (drawn first) should have diff --git a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp index 245cd14..4ada12d 100644 --- a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp +++ b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp @@ -149,24 +149,15 @@ void CreatureRenderer::drawMount(const Domain::Outfit &outfit, int dir, const std::vector *mount_sprites = &mount_data->sprite_ids; int mount_dir = dir % std::max(1, mount_data->pattern_x); - int mount_frame = 0; + // Select walk sprites if rider is walking if (animation_frame > 0 && !mount_data->walk_sprite_ids.empty()) { mount_sprites = &mount_data->walk_sprite_ids; - mount_frame = animation_frame % std::max(1, mount_data->walk_frames); - } else if (mount_data->has_frame_groups && !mount_data->idle_sprite_ids.empty()) { - mount_sprites = &mount_data->idle_sprite_ids; - mount_frame = ItemAnimation::getPhaseFromFrames( - static_cast(mount_data->idle_frames), anim_ticks.global_ms, - mount_data->has_animation_data ? &mount_data->frame_durations : nullptr, - mount_data->total_duration); - } else if (mount_data->frames > 1) { - mount_frame = ItemAnimation::getPhaseFromFrames( - static_cast(mount_data->frames), anim_ticks.global_ms, - mount_data->has_animation_data ? &mount_data->frame_durations : nullptr, - mount_data->total_duration); } + int mount_frame = selectMountFrame(mount_data, animation_frame, + anim_ticks.global_ms); + int mw = std::max(1, mount_data->width); int mh = std::max(1, mount_data->height); int ml = std::max(1, mount_data->layers); @@ -214,6 +205,26 @@ void CreatureRenderer::drawMount(const Domain::Outfit &outfit, int dir, } } +int CreatureRenderer::selectMountFrame(const IO::ClientItem *mount_data, + int animation_frame, int64_t global_ms) { + if (animation_frame > 0 && !mount_data->walk_sprite_ids.empty()) { + return animation_frame % std::max(1, mount_data->walk_frames); + } + if (mount_data->has_frame_groups && !mount_data->idle_sprite_ids.empty()) { + return ItemAnimation::getPhaseFromFrames( + static_cast(mount_data->idle_frames), global_ms, + mount_data->has_animation_data ? &mount_data->frame_durations : nullptr, + mount_data->total_duration); + } + if (mount_data->frames > 1) { + return ItemAnimation::getPhaseFromFrames( + static_cast(mount_data->frames), global_ms, + mount_data->has_animation_data ? &mount_data->frame_durations : nullptr, + mount_data->total_duration); + } + return 0; +} + void CreatureRenderer::emitPlaceholder(float screen_x, float screen_y, float size, float alpha) { const AtlasRegion *white = sprite_manager_.getAtlasManager().getWhitePixel(); diff --git a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.h b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.h index 22331c3..bc82df2 100644 --- a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.h +++ b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.h @@ -61,6 +61,9 @@ class CreatureRenderer { std::vector& missing_sprites); private: + static int selectMountFrame(const IO::ClientItem *mount_data, + int animation_frame, int64_t global_ms); + void drawMount(const Domain::Outfit &outfit, int dir, int animation_frame, float screen_x, float screen_y, float size, const TileColor &color, float alpha, diff --git a/ImguiMapEditor/Rendering/Tile/ItemRenderer.h b/ImguiMapEditor/Rendering/Tile/ItemRenderer.h index ee79e99..2cf036b 100644 --- a/ImguiMapEditor/Rendering/Tile/ItemRenderer.h +++ b/ImguiMapEditor/Rendering/Tile/ItemRenderer.h @@ -196,7 +196,7 @@ void ItemRenderer::queueAll( // ============================================================ // MULTI-PASS RENDERING (OTClient Painter Algorithm) // Pass 1: OnBottom (walls, pillars) — forward - // Pass 2: Common items — reverse (OTClient rbegin→rend) + // Pass 2: Common items — forward (bottom-to-top, painter's algorithm) // OnTop items rendered after creatures in TileRenderer // ============================================================ @@ -207,15 +207,10 @@ void ItemRenderer::queueAll( } } - // PASS 2: Common items — reverse (OTClient parity) - // Multi-tile lying corpses excluded (drawn pre-creature in TileRenderer) - for (auto it = items.rbegin(); it != items.rend(); ++it) { - const auto &entry = *it; + // PASS 2: Common items — forward (painter's algorithm: draw bottom first) + for (const auto &entry : items) { if (entry.type && entry.type->is_on_bottom) continue; if (entry.type && entry.type->is_on_top) continue; - if (entry.type && entry.type->is_lying_object && - (entry.type->width > 1 || entry.type->height > 1)) - continue; renderItem(entry); } diff --git a/ImguiMapEditor/Services/ItemCompositor.cpp b/ImguiMapEditor/Services/ItemCompositor.cpp index d295e42..103e666 100644 --- a/ImguiMapEditor/Services/ItemCompositor.cpp +++ b/ImguiMapEditor/Services/ItemCompositor.cpp @@ -51,8 +51,8 @@ ItemCompositor::getCompositedItemTexture(const Domain::ItemType *type) { for (int layer = 0; layer < layers; ++layer) { for (int cy = 0; cy < h; ++cy) { for (int cx = 0; cx < w; ++cx) { - size_t sprite_index = - (static_cast(layer) * h + cy) * w + cx; + size_t sprite_index = Utils::SpriteUtils::getSpriteIndex( + type, cx, cy, layer, 0, 0, 0, 0); if (sprite_index >= type->sprite_ids.size()) continue; diff --git a/ImguiMapEditor/UI/Utils/PreviewUtils.cpp b/ImguiMapEditor/UI/Utils/PreviewUtils.cpp index 63c2893..7eca8e5 100644 --- a/ImguiMapEditor/UI/Utils/PreviewUtils.cpp +++ b/ImguiMapEditor/UI/Utils/PreviewUtils.cpp @@ -47,8 +47,12 @@ CreaturePreviewResult GetCreaturePreview(Services::ClientDataService& clientData bool RenderPreviewCard(Rendering::Texture* texture, float size, bool is_selected, float rounding, float padding) { - ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 min = ImGui::GetCursorScreenPos(); + ImGui::Dummy(ImVec2(size, size)); + bool is_hovered = ImGui::IsItemHovered(); + bool is_clicked = ImGui::IsItemClicked(); + + ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 max(min.x + size, min.y + size); ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_FrameBg); @@ -56,8 +60,8 @@ bool RenderPreviewCard(Rendering::Texture* texture, float size, if (is_selected) { bg_col = ImGui::GetColorU32(ImGuiCol_Header); - border_col = IM_COL32(255, 200, 0, 255); // Gold - } else if (ImGui::IsMouseHoveringRect(min, max)) { + border_col = IM_COL32(255, 200, 0, 255); + } else if (is_hovered) { bg_col = ImGui::GetColorU32(ImGuiCol_HeaderHovered); } @@ -72,8 +76,7 @@ bool RenderPreviewCard(Rendering::Texture* texture, float size, ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, rounding); } - ImGui::Dummy(ImVec2(size, size)); - return ImGui::IsItemClicked(); + return is_clicked; } } // namespace MapEditor::UI::Utils diff --git a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp index bfc1c30..06aeb50 100644 --- a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp +++ b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp @@ -86,6 +86,45 @@ Rendering::Texture *TilesetGridWidget::getBrushTexture(const Brushes::IBrush *br return nullptr; } +void TilesetGridWidget::renderBrushCard(ImVec2 cursorPos, ImVec2 size, + Rendering::Texture *tex, + bool isSelected, bool isHovered, + bool isPulsing, float pulseElapsed) { + ImDrawList *dl = ImGui::GetWindowDrawList(); + constexpr float ROUNDING = 4.0f; + constexpr float PADDING = 2.0f; + ImVec2 rectMax(cursorPos.x + size.x, cursorPos.y + size.y); + + ImU32 bgCol = ImGui::GetColorU32(ImGuiCol_FrameBg); + if (isSelected) { + bgCol = ImGui::GetColorU32(ImGuiCol_Header); + } else if (isHovered) { + bgCol = ImGui::GetColorU32(ImGuiCol_HeaderHovered); + } + dl->AddRectFilled(cursorPos, rectMax, bgCol, ROUNDING); + + if (tex) { + ImVec2 imgMin(cursorPos.x + PADDING, cursorPos.y + PADDING); + ImVec2 imgMax(rectMax.x - PADDING, rectMax.y - PADDING); + dl->AddImageRounded((void *)(intptr_t)tex->id(), imgMin, imgMax, + ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, ROUNDING); + } + + if (isSelected) { + if (isPulsing && pulseElapsed < PULSE_DURATION) { + float pulse = 0.5f + 0.5f * std::sin(pulseElapsed * 8.0f); + ImU32 pulseCol = IM_COL32(static_cast(50 * (1 - pulse)), + static_cast(220 * pulse + 35), + static_cast(80 * pulse), 255); + float thickness = 2.0f + pulse * 2.0f; + dl->AddRect(cursorPos, rectMax, pulseCol, ROUNDING, 0, thickness); + } else { + dl->AddRect(cursorPos, rectMax, IM_COL32(100, 180, 255, 255), ROUNDING, + 0, 2.0f); + } + } +} + void TilesetGridWidget::render() { if (tilesetName_.empty()) { ImGui::TextDisabled(ICON_FA_BOX_OPEN " No tileset selected"); @@ -286,10 +325,12 @@ void TilesetGridWidget::renderBrushGrid() { ImGui::PushID(static_cast(i)); Rendering::Texture *tex = getBrushTexture(brush); - bool isSelected = false; - bool clicked = Utils::RenderPreviewCard(tex, getIconSize(), isSelected); + ImVec2 tileSize(getIconSize(), getIconSize()); + ImVec2 cursorPos = ImGui::GetCursorScreenPos(); + ImGui::Dummy(tileSize); + + renderBrushCard(cursorPos, tileSize, tex, false, ImGui::IsItemHovered()); - // Tooltip if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::Text("%s", brush->getName().c_str()); @@ -298,15 +339,13 @@ void TilesetGridWidget::renderBrushGrid() { ImGui::EndTooltip(); } - // Double-click handling if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(0)) { if (onBrushDoubleClicked_) { onBrushDoubleClicked_(bws.sourceTileset, brush->getName()); } } - // Click handling - if (clicked) { + if (ImGui::IsItemClicked()) { selectedBrushName_ = brush->getName(); if (brushController_) { brushController_->setBrush(const_cast(brush)); @@ -429,57 +468,23 @@ void TilesetGridWidget::renderBrushGrid() { ImGui::EndDragDropTarget(); } - ImDrawList *dl = ImGui::GetWindowDrawList(); - constexpr float CARD_ROUNDING = 4.0f; - constexpr float IMG_PADDING = 2.0f; - ImVec2 rectMax(cursorPos.x + tileSize.x, cursorPos.y + tileSize.y); - - // Rounded card background - ImU32 bgColor = ImGui::GetColorU32(ImGuiCol_FrameBg); - if (isSelected) { - bgColor = ImGui::GetColorU32(ImGuiCol_Header); - } else if (isHovered) { - bgColor = ImGui::GetColorU32(ImGuiCol_HeaderHovered); - } - dl->AddRectFilled(cursorPos, rectMax, bgColor, CARD_ROUNDING); - - // Rounded sprite - if (tex) { - ImVec2 img_min(cursorPos.x + IMG_PADDING, cursorPos.y + IMG_PADDING); - ImVec2 img_max(rectMax.x - IMG_PADDING, rectMax.y - IMG_PADDING); - dl->AddImageRounded((void*)(intptr_t)tex->id(), img_min, img_max, - ImVec2(0, 0), ImVec2(1, 1), - IM_COL32_WHITE, CARD_ROUNDING); - } - - // Selection border (with optional pulse animation) - if (isSelected) { - bool isPulsing = - !pulseBrushName_.empty() && brush->getName() == pulseBrushName_; - - if (isPulsing) { - float currentTime = ImGui::GetTime(); - if (pulseStartTime_ < 0) { - pulseStartTime_ = currentTime; - } - - float elapsed = currentTime - pulseStartTime_; - if (elapsed < PULSE_DURATION) { - float pulse = 0.5f + 0.5f * std::sin(elapsed * 8.0f); - ImU32 pulseColor = IM_COL32(static_cast(50 * (1 - pulse)), - static_cast(220 * pulse + 35), - static_cast(80 * pulse), 255); - float thickness = 2.0f + pulse * 2.0f; - dl->AddRect(cursorPos, rectMax, pulseColor, CARD_ROUNDING, 0, thickness); - } else { - pulseBrushName_.clear(); - pulseStartTime_ = -1.0f; - dl->AddRect(cursorPos, rectMax, IM_COL32(100, 180, 255, 255), CARD_ROUNDING, 0, 2.0f); - } - } else { - dl->AddRect(cursorPos, rectMax, IM_COL32(100, 180, 255, 255), CARD_ROUNDING, 0, 2.0f); + // Render card with pulse animation support + bool isPulsing = isSelected && !pulseBrushName_.empty() && + brush->getName() == pulseBrushName_; + float pulseElapsed = 0.0f; + if (isPulsing) { + float currentTime = ImGui::GetTime(); + if (pulseStartTime_ < 0) + pulseStartTime_ = currentTime; + pulseElapsed = currentTime - pulseStartTime_; + if (pulseElapsed >= PULSE_DURATION) { + pulseBrushName_.clear(); + pulseStartTime_ = -1.0f; + isPulsing = false; } } + renderBrushCard(cursorPos, tileSize, tex, isSelected, isHovered, + isPulsing, pulseElapsed); // Tooltip if (isHovered) { diff --git a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.h b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.h index b50a91a..3a7afa5 100644 --- a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.h +++ b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.h @@ -176,6 +176,14 @@ class TilesetGridWidget { void renderBrushGrid(); void applyFilter(); + /** + * Render a rounded card with sprite and optional selection/pulse border. + * Used by both cross-tileset and single-tileset rendering paths. + */ + void renderBrushCard(ImVec2 cursorPos, ImVec2 size, Rendering::Texture *tex, + bool isSelected, bool isHovered, + bool isPulsing = false, float pulseElapsed = 0.0f); + /** * Get preview texture for a brush (RawBrush or CreatureBrush). * Uses PreviewUtils for consistent texture retrieval. diff --git a/ImguiMapEditor/Utils/SpriteUtils.cpp b/ImguiMapEditor/Utils/SpriteUtils.cpp index 481e48f..8cb66b3 100644 --- a/ImguiMapEditor/Utils/SpriteUtils.cpp +++ b/ImguiMapEditor/Utils/SpriteUtils.cpp @@ -18,8 +18,32 @@ uint32_t SpriteUtils::getSpriteIndex(const IO::ClientItem *item, int w, int h, const int pattern_z_count = std::max(1, item->pattern_z); const int frame_count = std::max(1, item->frames); - // RME sprite index formula (no modulo on pattern inputs - they are already in - // range) + uint32_t index = 0; + index = (frame % frame_count); + index = index * pattern_z_count + pattern_z; + index = index * pattern_y_count + pattern_y; + index = index * pattern_x_count + pattern_x; + index = index * layers + layer; + index = index * height + h; + index = index * width + w; + + return index; +} + +uint32_t SpriteUtils::getSpriteIndex(const Domain::ItemType *item, int w, + int h, int layer, int pattern_x, + int pattern_y, int pattern_z, int frame) { + if (!item) + return 0; + + const int width = std::max(1, item->width); + const int height = std::max(1, item->height); + const int layers = std::max(1, item->layers); + const int pattern_x_count = std::max(1, item->pattern_x); + const int pattern_y_count = std::max(1, item->pattern_y); + const int pattern_z_count = std::max(1, item->pattern_z); + const int frame_count = std::max(1, item->frames); + uint32_t index = 0; index = (frame % frame_count); index = index * pattern_z_count + pattern_z; diff --git a/ImguiMapEditor/Utils/SpriteUtils.h b/ImguiMapEditor/Utils/SpriteUtils.h index 912d85d..4a64790 100644 --- a/ImguiMapEditor/Utils/SpriteUtils.h +++ b/ImguiMapEditor/Utils/SpriteUtils.h @@ -1,6 +1,7 @@ #pragma once #include "IO/Readers/DatReaderBase.h" #include "IO/SprReader.h" +#include "Domain/ItemType.h" #include #include #include @@ -15,29 +16,25 @@ namespace Utils { class SpriteUtils { public: /** - * Calculate sprite index for given pattern position. + * Calculate sprite index for given pattern position (ClientItem). * RME formula: * ((((((frame%frames)*pZ+pZ)*pY+pY)*pX+pX)*layers+layer)*height+h)*width+w - * @param item ClientItem with sprite dimensions - * @param w Width index (0 to width-1) - * @param h Height index (0 to height-1) - * @param layer Layer index (0 for base, 1 for template) - * @param pattern_x Pattern X index - * @param pattern_y Pattern Y index - * @param pattern_z Pattern Z index - * @param frame Animation frame - * @return Sprite index in sprite_ids array */ static uint32_t getSpriteIndex(const IO::ClientItem *item, int w, int h, int layer, int pattern_x, int pattern_y, int pattern_z, int frame); + /** + * Calculate sprite index for given pattern position (ItemType). + * Same formula as ClientItem overload. + */ + static uint32_t getSpriteIndex(const Domain::ItemType *item, int w, int h, + int layer, int pattern_x, int pattern_y, + int pattern_z, int frame); + /** * Load and decode a sprite from SprReader. * Returns decoded RGBA data, or empty vector on failure. - * @param spr_reader SprReader to load from - * @param sprite_id Sprite ID to load - * @return Decoded RGBA pixel data (32x32x4 bytes), empty on failure */ static std::vector loadDecodedSprite(const std::shared_ptr &spr_reader, From 8d17fb59205a92ec7fba520970303b1d3af9209d Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Thu, 4 Jun 2026 16:52:03 +0200 Subject: [PATCH 4/6] refactor(rendering/ui): refine animation and UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improved ping-pong animation calculation, refined mount sprite selection for idle/walk states, added safety checks for item rendering, and highlighted the selected brush in the tileset grid. - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — refactored ping-pong animation to use explicit timeline based on per-phase durations - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — updated mount sprite selection to use idle groups when available - `ImguiMapEditor/Services/ItemCompositor.cpp` — added safety check for empty `sprite_ids` in single-tile item rendering - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp` — added highlighting for selected brush in tileset grid --- .../Rendering/Animation/ItemAnimation.cpp | 54 ++++++++++++++----- .../Rendering/Tile/CreatureRenderer.cpp | 4 +- ImguiMapEditor/Services/ItemCompositor.cpp | 3 +- .../UI/Widgets/TilesetGridWidget.cpp | 4 +- 4 files changed, 50 insertions(+), 15 deletions(-) diff --git a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp index 8a75b71..0f6cf28 100644 --- a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp +++ b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp @@ -1,5 +1,6 @@ #include "ItemAnimation.h" #include +#include namespace MapEditor { namespace Rendering { @@ -54,19 +55,48 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, int tick = static_cast(global_ms / 500); return getPingPongPhase(tick + tile_offset + start_phase, frames); } - int total = static_cast(item.total_duration); - if (total <= 0) return start_phase; - int cycle_total = total * 2; - int raw_elapsed = static_cast((global_ms + static_cast(tile_offset) * 500) % cycle_total); - // Apply start_phase offset before reflection - int elapsed = (raw_elapsed + start_phase * (total / std::max(1, frames))) % cycle_total; - // Reflect: map [total, cycle_total) back to [0, total) - if (elapsed >= total) - elapsed = cycle_total - 1 - elapsed; - for (int phase = 0; phase < static_cast(item.frame_durations.size()); ++phase) { - int dur = getPhaseDuration(item, phase); + + // Build explicit ping-pong timeline from per-phase durations + const int num_phases = static_cast(item.frame_durations.size()); + if (num_phases == 0) return start_phase; + + // Forward timeline: [0, 1, 2, ..., N-1] + // Ping-pong timeline: [0, 1, ..., N-1, N-2, ..., 1] (omit duplicate endpoints) + std::vector timeline; + timeline.reserve(num_phases * 2); + for (int i = 0; i < num_phases; ++i) + timeline.push_back(i); + for (int i = num_phases - 2; i >= 1; --i) + timeline.push_back(i); + + // Sum per-phase durations for the cycle + std::vector phase_durs(num_phases); + int cycle_length = 0; + for (int i = 0; i < num_phases; ++i) { + phase_durs[i] = getPhaseDuration(item, i); + cycle_length += phase_durs[i]; + } + // Ping-pong cycle = forward + backward (endpoints shared) + int backward_length = 0; + for (int i = num_phases - 2; i >= 1; --i) + backward_length += phase_durs[i]; + cycle_length += backward_length; + + if (cycle_length <= 0) return start_phase; + + // Start offset: sum durations of first start_phase phases + int start_offset = 0; + for (int i = 0; i < start_phase && i < num_phases; ++i) + start_offset += phase_durs[i]; + + int elapsed = static_cast( + (global_ms + static_cast(tile_offset) * 500 + start_offset) % cycle_length); + + // Walk the timeline to find the active phase + for (int i = 0; i < static_cast(timeline.size()); ++i) { + int dur = phase_durs[timeline[i]]; if (elapsed < dur) - return phase % frames; + return timeline[i] % frames; elapsed -= dur; } return 0; diff --git a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp index 4ada12d..636d96d 100644 --- a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp +++ b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp @@ -150,9 +150,11 @@ void CreatureRenderer::drawMount(const Domain::Outfit &outfit, int dir, const std::vector *mount_sprites = &mount_data->sprite_ids; int mount_dir = dir % std::max(1, mount_data->pattern_x); - // Select walk sprites if rider is walking + // Select sprite source based on rider state (idle vs walk) if (animation_frame > 0 && !mount_data->walk_sprite_ids.empty()) { mount_sprites = &mount_data->walk_sprite_ids; + } else if (mount_data->has_frame_groups && !mount_data->idle_sprite_ids.empty()) { + mount_sprites = &mount_data->idle_sprite_ids; } int mount_frame = selectMountFrame(mount_data, animation_frame, diff --git a/ImguiMapEditor/Services/ItemCompositor.cpp b/ImguiMapEditor/Services/ItemCompositor.cpp index 103e666..4a0681f 100644 --- a/ImguiMapEditor/Services/ItemCompositor.cpp +++ b/ImguiMapEditor/Services/ItemCompositor.cpp @@ -25,7 +25,8 @@ ItemCompositor::getCompositedItemTexture(const Domain::ItemType *type) { } // Single-tile single-layer: load directly - if (type->width == 1 && type->height == 1 && type->layers <= 1) { + if (type->width == 1 && type->height == 1 && type->layers <= 1 && + !type->sprite_ids.empty()) { uint32_t sprite_id = type->sprite_ids[0]; auto sprite_data = Utils::SpriteUtils::loadDecodedSprite(spr_reader_, sprite_id); diff --git a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp index 06aeb50..0859ce2 100644 --- a/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp +++ b/ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp @@ -329,7 +329,9 @@ void TilesetGridWidget::renderBrushGrid() { ImVec2 cursorPos = ImGui::GetCursorScreenPos(); ImGui::Dummy(tileSize); - renderBrushCard(cursorPos, tileSize, tex, false, ImGui::IsItemHovered()); + bool isCrossSelected = brush->getName() == selectedBrushName_; + renderBrushCard(cursorPos, tileSize, tex, isCrossSelected, + ImGui::IsItemHovered()); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); From 21ee9f3555d7a5ee314c4c507a471ef8050cfe83 Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Thu, 4 Jun 2026 17:02:26 +0200 Subject: [PATCH 5/6] refactor(rendering): optimize heap allocations and fix direction logic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced dynamic heap-allocated std::vectors with fixed-size stack arrays to eliminate allocation overhead in animation phase calculation, and simplified creature direction mapping. - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — migrated timeline and phase duration buffers to stack arrays with constant size limits, removed unnecessary vector headers - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — simplified direction mapping logic to a direct modulo operation --- .../Rendering/Animation/ItemAnimation.cpp | 32 ++++++++++--------- .../Rendering/Tile/CreatureRenderer.cpp | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp index 0f6cf28..afaf979 100644 --- a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp +++ b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp @@ -1,6 +1,5 @@ #include "ItemAnimation.h" #include -#include namespace MapEditor { namespace Rendering { @@ -60,23 +59,26 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, const int num_phases = static_cast(item.frame_durations.size()); if (num_phases == 0) return start_phase; - // Forward timeline: [0, 1, 2, ..., N-1] - // Ping-pong timeline: [0, 1, ..., N-1, N-2, ..., 1] (omit duplicate endpoints) - std::vector timeline; - timeline.reserve(num_phases * 2); - for (int i = 0; i < num_phases; ++i) - timeline.push_back(i); - for (int i = num_phases - 2; i >= 1; --i) - timeline.push_back(i); - - // Sum per-phase durations for the cycle - std::vector phase_durs(num_phases); + // Per-phase durations (stack allocation — max 32 phases is more than enough) + constexpr int MAX_PHASES = 32; + int phase_durs[MAX_PHASES]; int cycle_length = 0; - for (int i = 0; i < num_phases; ++i) { + for (int i = 0; i < num_phases && i < MAX_PHASES; ++i) { phase_durs[i] = getPhaseDuration(item, i); cycle_length += phase_durs[i]; } - // Ping-pong cycle = forward + backward (endpoints shared) + + // Ping-pong timeline: [0, 1, ..., N-1, N-2, ..., 1] (omit duplicate endpoints) + // Timeline size is at most 2*N-2 + constexpr int MAX_TIMELINE = MAX_PHASES * 2 - 2; + int timeline[MAX_TIMELINE]; + int timeline_len = 0; + for (int i = 0; i < num_phases && i < MAX_PHASES; ++i) + timeline[timeline_len++] = i; + for (int i = num_phases - 2; i >= 1; --i) + timeline[timeline_len++] = i; + + // Backward portion cycle length int backward_length = 0; for (int i = num_phases - 2; i >= 1; --i) backward_length += phase_durs[i]; @@ -93,7 +95,7 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, (global_ms + static_cast(tile_offset) * 500 + start_offset) % cycle_length); // Walk the timeline to find the active phase - for (int i = 0; i < static_cast(timeline.size()); ++i) { + for (int i = 0; i < timeline_len; ++i) { int dur = phase_durs[timeline[i]]; if (elapsed < dur) return timeline[i] % frames; diff --git a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp index 636d96d..a349106 100644 --- a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp +++ b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp @@ -45,7 +45,7 @@ void CreatureRenderer::queue(const Domain::Creature *creature, float screen_x, } const int pattern_x = std::max(1, outfit_data->pattern_x); - int dir = direction < pattern_x ? direction : 2 % pattern_x; + int dir = direction % pattern_x; // Select sprite source and animation frame const std::vector *sprites = &outfit_data->sprite_ids; From 9b047a18903227224353e6ebfd36dac71e68ef26 Mon Sep 17 00:00:00 2001 From: glaszczkoldre Date: Thu, 4 Jun 2026 17:08:00 +0200 Subject: [PATCH 6/6] refactor(rendering): clamp animation phases and refine mount drawing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clamped animation phase counts to prevent potential overflows and added a pattern depth check when rendering mounts. - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — clamped loop iterations to `MAX_PHASES` to prevent potential out-of-bounds access - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — added `outfit_data->pattern_z > 1` check when deciding to render a mount --- .../Rendering/Animation/ItemAnimation.cpp | 14 +++++++------- ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp index afaf979..3bfa1dc 100644 --- a/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp +++ b/ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp @@ -59,28 +59,28 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, const int num_phases = static_cast(item.frame_durations.size()); if (num_phases == 0) return start_phase; - // Per-phase durations (stack allocation — max 32 phases is more than enough) + // Per-phase durations (stack allocation — clamp to prevent overflow) constexpr int MAX_PHASES = 32; + const int effective_phases = std::min(num_phases, MAX_PHASES); int phase_durs[MAX_PHASES]; int cycle_length = 0; - for (int i = 0; i < num_phases && i < MAX_PHASES; ++i) { + for (int i = 0; i < effective_phases; ++i) { phase_durs[i] = getPhaseDuration(item, i); cycle_length += phase_durs[i]; } // Ping-pong timeline: [0, 1, ..., N-1, N-2, ..., 1] (omit duplicate endpoints) - // Timeline size is at most 2*N-2 constexpr int MAX_TIMELINE = MAX_PHASES * 2 - 2; int timeline[MAX_TIMELINE]; int timeline_len = 0; - for (int i = 0; i < num_phases && i < MAX_PHASES; ++i) + for (int i = 0; i < effective_phases; ++i) timeline[timeline_len++] = i; - for (int i = num_phases - 2; i >= 1; --i) + for (int i = effective_phases - 2; i >= 1; --i) timeline[timeline_len++] = i; // Backward portion cycle length int backward_length = 0; - for (int i = num_phases - 2; i >= 1; --i) + for (int i = effective_phases - 2; i >= 1; --i) backward_length += phase_durs[i]; cycle_length += backward_length; @@ -88,7 +88,7 @@ int ItemAnimation::getPhase(const Domain::ItemType &item, int64_t global_ms, // Start offset: sum durations of first start_phase phases int start_offset = 0; - for (int i = 0; i < start_phase && i < num_phases; ++i) + for (int i = 0; i < start_phase && i < effective_phases; ++i) start_offset += phase_durs[i]; int elapsed = static_cast( diff --git a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp index a349106..9cdccb0 100644 --- a/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp +++ b/ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp @@ -73,7 +73,7 @@ void CreatureRenderer::queue(const Domain::Creature *creature, float screen_x, // Draw mount before rider int z_pattern = 0; - if (outfit.lookMount > 0) { + if (outfit.lookMount > 0 && outfit_data->pattern_z > 1) { z_pattern = 1; drawMount(outfit, dir, animation_frame, screen_x, screen_y, size, creature_color, alpha, anim_ticks, missing_sprites);