Skip to content

feat(items): item effects from the model files (phase 4c3) - #93

Open
Mosch0512 wants to merge 7 commits into
mainfrom
feat/item-effects
Open

Mosch0512 wants to merge 7 commits into
mainfrom
feat/item-effects

Conversation

@Mosch0512

@Mosch0512 Mosch0512 commented Sep 29, 2026 •

Copy link
Copy Markdown
Owner

Phase 4c3 of the data-driven items (design doc D24, see docs/superpowers/specs/2026-09-25-data-driven-items-design.md): what item models do before they are drawn comes from the item model files instead of the type chain of RenderPartObjectEffect. Builds on phase 4c2 (sven-n#658). With it, phase 4c (glow, render styles, effects) is complete.

It also adds the plan beyond items: a new roadmap document for effects, skills, monsters and NPCs, and how their data is shared and edited.

What changes

  • New "itemEffect" value in src/bin/Data/Items/Models/*.json (42 models have one). An item effect is what an item does every frame before it is drawn; the effect types of the planned effect catalogue (FX1, effectTypes) are what it creates instances of, so the two keep different names:
    { "number": 37, "file": "Data/Item/wing09.bmd", "textureFolders": ["Item"], ..., "itemEffect": "wingOfEternal" }
  • Item effects Render/Items/ItemEffects (namespace Render::Items::ItemEffects, so it does not hide Render::Effects, the effect registry): 28 of the 29 item branches of the chain (42 items) become 27 named effects, the code moved unchanged. The Siege Potion and the Contract share one. An effect runs before the model is drawn and:
    • places sprites, particles and lightning on bones (Devil's Key and Invitation, Rena, the Wings of Darkness, the third-tier wings);
    • changes values of the drawing: a pulsing glow mesh (wingsOfDragon, staffOfKundun, divineSet, …), a mesh hidden by level (hiddenMeshByLevel, hideMesh1), or the level potions glow like (potion);
    • or draws the model itself (the Dark Lord scrolls, fruits, spirit, bloodBone, invisibilityCloak, firecracker, gmGift, meshesPerLevel).
  • The shine below +3 of 10 items (the light scaled, then two shine passes) becomes values of their render style, which the drawing code applies. The seals (light 0.9), the Illusion Sorcerer Covenant and the Cursed Castle water have a style already; the Jewel of Harmony and the Moonstone Pendant get harmonyShine, which only shines (138 styles now).
  • Socket seeds and spheres (the 39 items of the last branch) and zen need no effect: they get "glow": {"level": 0}, which is what their level checks did. The checks go.
  • Stays in code: the effects of the event models of level variants (no model entries yet), in ApplyEventModelEffect in ZzzObject.cpp.
  • Check: an item effect that does not exist is a warning when the models are opened, like an unknown render style. The model loader gets the checks of both look names from ZzzOpenData (LookNames), so the data layer does not call the drawing code. A test checks that the item effects of the shipped models exist.
  • ItemModelTable (ItemModelTable.h): one value per item type, taken from the item model database after each build of it. The render styles and the effects both use it (the table code of 4c2 moves there).
  • Effect step: ApplyPartObjectEffect runs the effect of the item's model entry, or for the event models theirs; RenderPartObjectEffect returns when it drew the model.
  • MuEditor: a read-only "Looks" section above the item table. For the selected item it shows the model file, glow, render style and effect; each look lists the items that share it (in a box of fixed height), and clicking one selects it and scrolls the table to it, clearing the search when it hides the item. It only reads the model data, rebuilt when the selection, the model data or the items change (the item database counts its changes now). The labels are in Editor.en.resx only; other languages fall back to English, as for the bmd import and export.
  • Removed: about 560 lines of the chain in ZzzObject.cpp, and the zen and seal/jewel checks of the plain drawing below +3.

Nothing changes while playing.

Plan beyond items (docs)

  • New docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md:
    • D25 Looks in data: items (and later skills, monsters and NPCs) name their looks. What a named look is made of moves into data later, as building blocks.
    • D26 Shared definitions: defined once with a name and referenced by it. Editors show where a definition is used, warn with that list before a shared definition changes, and make copies for variants.
    • Bones: effects place things on bones, and looks store bones as numbers. Of the 778 item model files, 340 have readable biped names; the rest have export names like Bone01 or zx12, and in 396 files the bone called "BoneNN" is not bone NN.
    • The areas in order, each with its own design document when it starts: FX1 effect catalogue, item phase 13 (looks in data), SK1 skills, SK2 skill looks, MN monsters and NPCs, FX2 effect behavior as data, later map objects, buffs, pets and sounds.
    • Shared editor parts: the look editor with bone picking, the effect browser and the effect editor.
  • Items design: new phase 13 "Looks in data" (depends on FX1); the cleanup is now phase 14; phase 6 and section 9 describe the look editor; 4c3 is described with its numbers.
  • docs/item-data.md: explains itemEffect, the shine below +3 of the render styles, the glow level 0 of seeds, spheres and zen, the warning, and the Looks section of the item editor.

Verification

A temporary recorder (not part of this PR) marked every branch of the old chain and every drawing path after it (the plain drawing and the shine below +3, the level glow). It ran RenderPartObjectEffect for every model slot:

  • on an item object (set up with ItemObjectAttribute, like the game's) and on a player object;
  • at the levels 0, 3, 7, 9, 13, 14 and 15.

It recorded the branch of the chain, the level after it and the drawing path that follows, and did the same for the new code (the effects mapped to their first branch).

All 141,231 other records are the same. The only differences:

  • 79 item slots on a player object: the new code does what it does on the item object, where the old checks looked at the object's item (o->Type). None of these items is drawn as a part of a character: weapons and wings are link objects with their own object, and potions, seeds, seals and scrolls are never on a character. The same holds for the look calls: the effect code, the inventory and the ground items draw with o->Type equal to the model.
  • Zen: it reaches the drawing with level 0 from its glow level, where the old code kept its level and drew it plainly by its type. It is the plain drawing at every level either way.

The recording was made before the review moved the shine below +3 into the render styles. That move is checked by a test instead: each of the 10 items has the light factor and the two shine passes of its old branch, and the drawing code applies them in the old order (the light, the model, the passes).

Tests

  • test_item_model_json.cpp: write/read of itemEffect (after renderStyle), errors for values that are not a name.
  • test_item_model_problems.cpp: the warning for an item effect that does not exist.
  • test_item_model_files.cpp:
    • the effects of the shipped models exist;
    • spot checks of the shipped effects (wings, Devil's items, potions, shared effects);
    • the shine below +3 of the 10 items, with the values of the old code, and none for other items;
    • the glow level 0 of seeds, spheres and zen;
    • ItemEffects::Apply changes the level of potions and the hidden mesh by level, returns "drawn" for effects that draw the model and "none" for other items and models;
    • ApplyPartObjectEffect: the item's effect first, the event model only when the item has none;
    • the effects follow a new build of the database.
  • All 382 tests pass locally (Windows, Release, editor build). clang-format 21.1.8 and cppcheck 2.13 are clean; the changed files also compile with gcc (Linux, editor off).

Test plan

  • Recording of old and new code compared
  • The game builds and the data loads in the tests (889 models)
  • In game: third-tier wings, Wings of Darkness, Devil's Key and Invitation, potions, the Dark Lord scrolls, seals and Jewel of Harmony below +3
  • In MuEditor: the Looks section of the item editor

- Items keep naming their looks (render style, effects); what a named
  look is made of moves from code into Data/Effects later, as lists of
  building blocks (D25, new phase 13 "Looks in data"). 4c3 names its
  effects like 4c2 names its styles.
- A named look is shared: the editor lists the items that use a look and
  warns with that list before a change to a shared look is saved.
- Section 9 gets the look editor (preview, bones by number and name,
  bone picking, items of a look); phase 6 shows the looks of an item and
  the items that share a look, read only.
- Bones: the names in the .bmd files are export names (biped names on
  armor, otherwise Bone01, Box02, zx12, ...; in 396 of the 778 item model
  files the bone called BoneNN is not bone NN), so looks use bone
  numbers and the editor shows both.
- The cleanup (now phase 14) writes follow-up design documents: the
  particle and effect system of ZzzEffect as data, and the looks of
  monsters and NPCs (their glow, the helper NPC plate look, the player
  transformations).
…D26)

- D26: anything that several things use is defined once, with a name,
  and referenced by that name; editors show where it is used, warn with
  the list of users before a shared definition changes, make copies for
  variants, rename with the references, and refuse to delete what is
  still used.
- New section "Beyond items": the effect code has about 3,950 callers
  (maps and monsters, characters and items, skills, network), so the
  areas that use it are planned together, each with its own design
  document: FX1 effect catalogue (names and creation values of effect,
  particle, lightning and sprite types), SK1 skills as data, SK2 skill
  looks, MN monsters and NPCs, FX2 effect behavior as data, later map
  objects, buffs, pets and sounds. Items, skills, monsters and NPCs share
  one look format (D25) and one look editor.
- The phases table lists these areas; item phase 13 depends on FX1.
- 4c3 brings a read-only look view to MuEditor (the looks of an item,
  the items of a look); phase 6 builds on it.
The parts of the items design that go beyond items move into their own
document, 2026-09-29-data-driven-content-roadmap-design.md, which stays
while its areas are open (the items design is removed after its last
phase):

- the shared decisions D25 (looks in data, one look format for items,
  skills, monsters and NPCs) and D26 (shared definitions), with their
  numbers kept;
- the bones: export names in the item models, readable biped names on
  characters, bone numbers in the data;
- the areas and their order (FX1 effect catalogue, item phase 13, SK1
  skills, SK2 skill looks, MN monsters and NPCs, FX2 effect behavior,
  later map objects, buffs, pets and sounds), each with its own design
  document when its work starts;
- the shared editor parts (look editor, effect browser and editor).

The items design keeps short pointers to it, and its cleanup moves what
later areas still need into the roadmap.
What item models do besides being drawn comes from "effect" in
Data/Items/Models instead of the type chain of RenderPartObjectEffect:

- The 29 item branches of the chain (80 items) become 27 named effects
  in Render/Items/ItemEffects.cpp, moved unchanged; the Siege Potion and
  the Contract share one. An effect places sprites, particles and
  lightning on bones (the Devil's Key and Invitation, Rena, the wings),
  changes values of the drawing (a pulsing glow mesh, a mesh hidden by
  level, the level potions glow like) or draws the model itself (the
  Dark Lord's scrolls, fruits, spirit, ...).
- The shine of 10 items below +3 becomes three effects: harmonyShine
  (Jewel of Harmony, Moonstone Pendant, Illusion Sorcerer Covenant),
  sealShine (the seals) and cursedCastleWater.
- The socket seeds and spheres and zen get "glow": {"level": 0}, which
  is what their level checks did; the checks go.
- The effects of the event models of level variants (no model entries
  yet) stay in ZzzObject.cpp, in ApplyEventModelEffect.
- An effect that does not exist is a warning when the models are opened;
  the model loader gets the checks of the look names from ZzzOpenData.
- ItemModelTable (ItemModelLookup.h): one value per item type, taken
  from the item model database after each build of it, for the render
  styles and the effects.
- MuEditor: a read-only view of the looks above the item table, with the
  model file, glow, render style and effect of the selected item and the
  items that share a look (clicking one selects it).

A temporary recorder ran RenderPartObjectEffect for every model slot, on
an item object (set up with ItemObjectAttribute) and on a player object,
at the levels 0, 3, 7, 9, 13, 14 and 15, and recorded the branch of the
chain, the level after it and the drawing path that follows; the same
with the new code. All the same, except: on a player object the new code
does what it does on the item object, where the old checks looked at the
object's item (79 item slots, none of them drawn as a character part),
and zen reaches the drawing with level 0 from its glow level, which draws
it plainly at every level as before.

@Mosch0512 Mosch0512 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: changes requested. (Posted as a comment: GitHub does not allow requesting changes on your own PR.)

Moving the chain itself looks right. I checked the effect of every model against the removed branches (all 52 match), each effect's Applied/Drawn against the old returns, and the glow level 0 of seeds, spheres and zen against the old level checks. Link objects, the inventory and ground items draw with o->Type == Type, so looking the effect up by Type instead of o->Type changes nothing in game.

Required:

  1. Looks list: a click often does not select or show the item (search filter, scroll position).
  2. An open Looks list squeezes the item table.
  3. ApplyBloodBone: b instead of Models[o->Type].
  4. ItemRenderStyles.cpp still includes <span> and <vector> (CODING_RULES rule 7).
  5. ItemModelTable in its own file (rule 6).
  6. The Looks section in the usage docs, "Editing items (MuEditor)" in docs/item-data.md (rule 11).

The other inline comments are non-blocking.

{
if (usesLook(models[itemType]) && ImGui::Selectable(g_ItemDatabase.GetLogName(itemType).c_str(), false))
{
CItemEditorTable::RequestScrollToIndex(itemType);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required. A click here often does not select or show the item:

  • ItemEditorTable.cpp only handles the request when the item is in m_filteredItems. With a search active (search "seal", select Seal of Ascension, click Master Seal of Wealth under sealShine) or for an item without a name, the request is dropped and nothing happens.
  • When the row is found, SetScrollY(GetTextLineHeightWithSpacing() * (row + 1)) assumes rows one text line high, but the rows are InputInt fields with frame and cell padding, about 4px taller each. For items of groups 12 to 14 (rows in the thousands) the table stops hundreds of rows short, so the selected row is off screen.

The description says clicking selects the item in the table. Suggestion: always set the selection (and say so, or clear the search, when the search hides the item), and scroll with clipper.IncludeItemByIndex(row) before Step() plus ImGui::SetScrollHereY() when that row is drawn.

ImGui::Separator();

// The looks of the selected item (read only).
CItemEditorLooks::Render(m_selectedRow);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required. The Looks section sits above the item table in the same window. The table scrolls (ScrollY) with an outer height of 0, which Dear ImGui aligns to the bottom of the window, so it only gets the height that is left. Opening a long list (the socketSeedSphere style is used by 30 items) shrinks the table down to 1px until the list is closed. Suggestion: draw the lists in a child or list box with a fixed height, or give the table a minimum height.

Comment thread src/source/Render/Items/ItemEffects.cpp Outdated
b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU,
o->BlendMeshTexCoordV, o->HiddenMesh);
Vector(.9f, .1f, .1f, b->BodyLight);
Models[o->Type].StreamMesh = 0;

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required. Models[o->Type] is how the old chain named the drawn model; the effect is now picked by Type, and b is &Models[Type]. When o->Type != Type (like the player object in your recorder), this sets and resets StreamMesh of another model, and the Blood Bone draws its bright pass without the stream mesh. b->StreamMesh, as in ApplyBillOfBalrog, fixes it.

}
// The render style of every item type, taken from the item model database
// after each build of it.
ItemModelTable<const RenderStyle*> g_itemStyles{[](const Data::Items::ItemModelDefinition& model)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required. With the table moved to ItemModelTable, this file no longer uses std::span or std::vector, but still includes <span> and <vector> (lines 12 and 14). CODING_RULES rule 7: "Keep includes minimal — only what the file actually uses."

// One value per item type, taken from the item model database after each
// build of it (the database counts its builds), e.g. the render style of
// every item. Drawing looks values up by model slot.
template <typename T> class ItemModelTable

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Required. CODING_RULES rule 6: "Each new class or significant struct/type gets its own file, named after the class." ItemModelTable is a class that two systems use (render styles and effects), so it would go into ItemModelTable.h; ItemModelLookup.h keeps the lookup functions.

Comment thread src/source/Render/Items/ItemEffects.cpp Outdated
RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f);
}

void RenderHarmonyShine(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType, float* Light)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. RenderHarmonyShine and RenderSealShine only differ in the light factor (1 and 0.9), and ApplyDevilsKey and ApplyDevilsInvitation only in the bones (1, 2 and 9, 10). One function each, taking the factor or the bones, would do. It is moved code, so this can also wait for phase 13.

Comment thread src/source/Render/Items/ItemEffects.cpp Outdated
Vector(1.f, 1.f, 1.f, b->BodyLight);
b->StreamMesh = 0;
b->RenderMesh(0, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV);
if (Level == 1)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. The empty if (Level == 1) {} comes from the old code; a switch with cases 2 and 3 says the same without it.

Comment thread docs/item-data.md Outdated
- change values of the drawing: a pulsing glow mesh (`wingsOfDragon`,
`wingsOfSoul`, `redSpirit`, `staffOfKundun`, `divineSet`), a mesh hidden
by level (`hiddenMeshByLevel` of the Siege Potion and the Contract,
`hideMesh1`), the level potions glow like (`potion`: +7 with any level);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. "+7 with any level": ApplyPotion only changes levels above 0, a +0 potion stays at 0 (your test checks that too). Maybe "+7 at every level above 0".

`RenderPartObjectEffect` (about 80 item branches, 31 distinct).
- **4c3 Item effects:** `effect` names what the model does besides
being drawn, from the type chain of `RenderPartObjectEffect`: 29 item
branches for 80 items become 27 effects in

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. I count 81 items: 42 with an effect that runs before the drawing (52 models with an effect minus the 10 shines) and 39 socket seeds and spheres (12/60–65, 70–72, 100–129). The PR description says 80 too.

Comment thread src/source/Engine/Object/ZzzObject.cpp Outdated
Render::Items::Effects::Result effect = Render::Items::Effects::Apply(b, o, Type, Alpha, Level);
if (effect == Render::Items::Effects::Result::None)
{
effect = ApplyEventModelEffect(b, o, Type, Level, ItemLevel);

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. The tests call Effects::Apply directly; how RenderPartObjectEffect puts the pieces together is not tested (event models only when the item has no effect, the return after Drawn, the shine only below +3). If it is cheap, a small test of that order would keep it from breaking unnoticed.

- The shine of 10 items below +3 belongs to their render style: values
  (the light factor and two shine passes) that the drawing code in
  RenderPartObjectEffect applies, instead of three effects that called
  back into RenderPartObjectBody. The seals, the Illusion Sorcerer
  Covenant and the Cursed Castle water have a style already; the Jewel of
  Harmony and the Moonstone Pendant get harmonyShine (drawn plainly
  otherwise). "effect" now only means what runs before the drawing.
- ItemModelTable in its own header; ItemRenderStyles.cpp drops the
  includes it no longer uses.
- The Blood Bone sets the stream mesh of the drawn model (b), as the
  Bill of Balrog does.
- ApplyPartObjectEffect: the effect of the item's model entry, or of the
  event model, as a function of its own with a test of that order.
- A look check that is left out of LookNames reports all names instead
  of crashing.
- The level switch of meshesPerLevel without its empty branch.
- MuEditor Looks: clicking an item always selects it (and clears the
  search when it hides the item); the table scrolls to its row with the
  clipper instead of guessing the row height; the lists sit in boxes of
  fixed height and the table keeps a minimum height; the lists and the
  glow text are built when the selection or the model data changes, not
  every frame.
- docs: the Looks section under "Editing items (MuEditor)", the shine
  under the render styles, 81 items (42 with an effect, 39 seeds and
  spheres), "+7 at every level above 0".
What an item does every frame before it is drawn is an item effect:
"itemEffect" in Data/Items/Models, Render::Items::ItemEffects in the
code, ItemEffectUnknown for a name that does not exist. The namespace no
longer hides Render::Effects (the effect registry) inside Render::Items,
and "effect" stays free for the effect types of the effect catalogue
(FX1, effectTypes in the data), which item effects create instances of.

@Mosch0512 Mosch0512 left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: ready to merge. (Posted as a comment: GitHub does not allow approving your own PR.)

Re-review of 121e0c4 and 2e118d8. All six required items of the first review are fixed:

  1. Looks list: a click always selects the item and clears the search when it hides it; the table scrolls with clipper.IncludeItemByIndex and SetScrollHereY.
  2. The lists are list boxes of at most 8 rows, and the table keeps a minimum height.
  3. ApplyBloodBone uses b->StreamMesh.
  4. ItemRenderStyles.cpp no longer includes <span> and <vector>.
  5. ItemModelTable has its own ItemModelTable.h.
  6. The Looks section is described under "Editing items (MuEditor)".

The non-blocking ones are fixed too: LookNames defaults to NoLookExists; the shine below +3 is now values of the render style (checked against the old code for all 10 items), so ItemEffects no longer calls back into ZzzObject.cpp; the rename to itemEffect / Render::Items::ItemEffects; the Looks cache; the switch in meshesPerLevel; both doc fixes (81 items, "+7 at every level above 0"); and the test of ApplyPartObjectEffect. Only ApplyDevilsKey / ApplyDevilsInvitation are still two copies, which is fine until phase 13.

Two new notes inline, both non-blocking.

void Refresh(int itemType, const ItemModelDefinition& model)
{
const int databaseVersion = g_ItemModelDatabase.GetVersion();
if (g_cache.itemType == itemType && g_cache.databaseVersion == databaseVersion)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. The cache only follows the model database, but the names in the lists (GetLogName, the neutral name) come from g_ItemDatabase, which the editor changes without rebuilding the model database. After renaming an item, moving one with the Index column (OnItemsSwapped) or Import from bmd, an open list keeps the old names until another item is selected; after a move, clicking an entry selects whatever now has that item type. Resetting the cache on those edits (or also keying it on a change counter of the item database) would keep it current.

}

// Styles that shine below +3.
constexpr RenderStyle(const char* styleName, StyleFunction recipe, const ShineBelowPlus3& shineBelowPlus3)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking. This constructor keeps the address of a const& parameter, while the textured one below takes a pointer. RenderStyles[] is const, not constexpr, so a later entry written as {"newShine", RenderPlainly, ShineBelowPlus3{...}} compiles and leaves shine pointing at a temporary that is gone after the initializer. Taking const ShineBelowPlus3* in both constructors, as the textured one does, makes the need for a named constant visible and keeps the two the same.

- The item database counts its changes (GetVersion); the Looks section
  of the item editor also rebuilds its lists when the items change, so
  a renamed or moved item or a bmd import shows at once.
- The render style constructor for the shine below +3 takes a pointer,
  like the textured one, so a style cannot keep the address of a
  temporary shine.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant