Parcourir la source

fix(appearance): select class-specific item art

Choose the installed class-qualified arrangement for each equipped item, falling back to the generic row only when the character-specific entry is absent.

Apply plug-owned art as bounded overlay arrangements instead of replacing the base arrangement. This preserves armor and ornament composition while keeping gear-art and material overrides in their native stages.
Thomas Shields il y a 3 semaines
Parent
commit
39215d40e8

+ 16 - 6
Sunrise/src/middleware/datagen/character_record/appearance/character_appearance_render.cpp

@@ -1,3 +1,5 @@
+#include <algorithm>
+
 #include "../../../../state/build_data/runtime.h"
 #include "internal.h"
 
@@ -96,8 +98,10 @@ void apply_material_pairs(const details::Definition& detail,
     }
 }
 
-/** Applies plug-owned gear art and arrangements after the base item. */
-void apply_plug_art(const Equipped& equipped, layout::RenderEntry& entry) noexcept {
+/** Applies plug-owned gear art and appends its class-qualified overlay arrangement. */
+void apply_plug_art(const Equipped& equipped,
+                    state::CharacterClass characterClass,
+                    layout::RenderEntry& entry) noexcept {
     for (std::size_t lane = 0; lane < equipped.laneCount; ++lane) {
         details::Definition plug{};
         if (equipped.plugs[lane] == details::kUnavailableItemIndex
@@ -109,8 +113,13 @@ void apply_plug_art(const Equipped& equipped, layout::RenderEntry& entry) noexce
         if (plug.gearArtIndex != details::kUnavailableArtIndex) {
             entry.art[layout::kGearArtSlot] = plug.gearArtIndex;
         }
-        if (plug.artArrangementIndex != details::kUnavailableArtIndex) {
-            entry.art[layout::kArtArrangementSlot] = plug.artArrangementIndex;
+        const std::uint16_t overlay = select_art_arrangement(plug, characterClass);
+        if (overlay != details::kUnavailableArtIndex) {
+            const auto empty = std::find(
+                entry.overlays.begin(), entry.overlays.end(), layout::kEmptyDefinitionIndex);
+            if (empty != entry.overlays.end()) {
+                *empty = overlay;
+            }
         }
     }
 }
@@ -143,6 +152,7 @@ bool resolve_equipped(const family4::loadout::SlottedInstance& slotted,
 
 /** Fills each equipped render row with its instance, definition, art and material pairs. */
 bool apply_render(const family4::loadout::ResolvedInstances& instances,
+                  state::CharacterClass characterClass,
                   layout::Appearance& appearance) noexcept {
     for (std::size_t index = 0; index < instances.itemCount; ++index) {
         const family4::loadout::SlottedInstance& slotted = instances.items[index];
@@ -160,8 +170,8 @@ bool apply_render(const family4::loadout::ResolvedInstances& instances,
         // Both art lookups accept 0, so an item with no art block keeps the empty sentinel
         // rather than taking art row 0.
         entry.art[layout::kGearArtSlot] = detail.gearArtIndex;
-        entry.art[layout::kArtArrangementSlot] = detail.artArrangementIndex;
-        apply_plug_art(equipped, entry);
+        entry.art[layout::kArtArrangementSlot] = select_art_arrangement(detail, characterClass);
+        apply_plug_art(equipped, characterClass, entry);
         apply_material_pairs(detail, equipped, entry);
     }
     return true;

+ 13 - 0
Sunrise/src/middleware/datagen/character_record/appearance/internal.h

@@ -28,6 +28,18 @@ struct Equipped {
     std::size_t laneCount{};
 };
 
+/** Selects class-qualified art when present, otherwise the definition's generic arrangement. */
+[[nodiscard]] constexpr std::uint16_t
+select_art_arrangement(const details::Definition& detail,
+                       state::CharacterClass characterClass) noexcept {
+    const std::size_t classSlot = static_cast<std::size_t>(characterClass) + 1U;
+    if (classSlot < detail.artArrangementIndices.size()
+        && detail.artArrangementIndices[classSlot] != details::kUnavailableArtIndex) {
+        return detail.artArrangementIndices[classSlot];
+    }
+    return detail.artArrangementIndices.front();
+}
+
 /**
  * Resolves one equipped instance to its detail and the plugs its sockets hold.
  * The resolver has already applied the authored or native-default socket policy, so a lane that
@@ -51,6 +63,7 @@ void apply_sentinels(layout::Appearance& appearance) noexcept;
  * @return True when every instance addresses a render row.
  */
 [[nodiscard]] bool apply_render(const family4::loadout::ResolvedInstances& instances,
+                                state::CharacterClass characterClass,
                                 layout::Appearance& appearance) noexcept;
 
 /**

+ 1 - 1
Sunrise/src/middleware/datagen/character_record/character_record_encoder.cpp

@@ -48,7 +48,7 @@ constexpr std::size_t kPreviewFlagOffsets[]{8, 9};
     output.unusedFloatA = 1.0F;
     output.unusedFloatB = static_cast<float>(light);
     output.light = static_cast<float>(light);
-    if (!appearance::apply_render(instances, output)
+    if (!appearance::apply_render(instances, character.characterClass, output)
         || !appearance::apply_stats(instances, light, output)
         || !appearance::apply_ability_buckets(character, instances, output)) {
         return false;