From 173808e4119f0b967ae1191444160849eb9f16b1 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Tue, 29 Sep 2026 21:26:33 +0200 Subject: [PATCH 1/9] docs(items): looks in data and the look editor (D25, phase 13) - 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). --- .../2026-09-25-data-driven-items-design.md | 49 ++++++++++++++++--- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md index 205331c98..6892cf83b 100644 --- a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md +++ b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md @@ -54,7 +54,8 @@ pattern later but are not part of this work. | D19 | Editor sync | Every item editor change goes into the database right away (phase 2), so the editor and the database never differ. Moving the editor fully onto the database stays in phase 6. | | D22 | Model data files | Model and display data (model file, textures, inventory and ground display, cloth, effects) lives in separate files, `Data/Items/Models/GroupNN_*.json`, one per item group, items by `number`. It is client-only: the item files keep what client and server share, and only those take part in the OpenMU exchange. Separate files also keep the item files small and let the model editor and the stats editor change different files. | | D23 | Model slots | Two steps. **Own model slots (phase 4a–4c):** every item keeps its own model slot, `MODEL_ITEM + item type`, as today; the data only says which file is loaded into it, so the ~100 places that compute `+ MODEL_ITEM` / `- MODEL_ITEM` stay unchanged. **Shared models (phase 4d, right after them):** models become entries of their own, and items reference them by name, so several items can share one model (loaded once). The code then asks the item database for an item's model slot, and objects keep their item type instead of computing it back from the model. Phase 12 (level variants as items of their own) then shares models without loading them twice. | -| D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and a list of particle effects (`RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. | +| D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and a list of particle effects (`RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. The named looks are code for now; D25 moves what they are made of into data. | +| D25 | Looks in data | Items keep referencing their looks by name (the render style and the effects; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/` (phase 13): a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; the item data written in phase 4 stays valid. A named look is shared: changing it changes every item that uses it, so the editor lists the items of a look and warns with that list before a change to a shared look is saved. The particle and effect types themselves (the particle system of `ZzzEffect`) become data as well, as a project with its own design document (phase 14). | | D21 | New item groups | *To discuss again when we reach phase 12.* Items may move into new groups (e.g. 16 = jewels, 17 = orbs) for the new client, while original Season 6 clients keep the old ids. Moved items keep their original id as a legacy id; OpenMU's Season 6 item serializer sends the legacy id, a serializer for the new client sends the new id. Planned after phases 3, 4 and OpenMU PR A, when little code depends on group numbers any more. | ## Current state @@ -378,7 +379,8 @@ table: | **Requirements** | Level and stat requirements, allowed classes. | | **Rules** | Flags as a matrix (items × tradable, droppable, storable, sellable, …) for bulk editing. | | **Categories** | Tag-centered view: pick a tag, see and change its items. | -| **Model and visuals** | Model file picker for `Data\Item\*.bmd` with 3D preview, texture folder, inventory and ground display values, cloth flag, and glow, render style and effects chosen from lists (D24). | +| **Model and visuals** | Model file picker for `Data\Item\*.bmd` with 3D preview, texture folder, inventory and ground display values, cloth flag, and glow, render style and effects chosen from lists (D24), each with the list of the other items that use the same look. | +| **Look editor** (phase 13) | Edits a named look (D25) as its list of building blocks, with a live preview of the model and its animations. Shows the skeleton with every bone's number and its name from the `.bmd` file, highlights the bones the look uses, and picks bones by clicking them instead of looking up numbers in the model file. Lists all items that use the look; saving a change to a look that other items share shows a warning with the list of those items. A new look starts as a copy of another. | | **Translations** | Items × languages grid, filter for missing translations. | | **OpenMU sync** | Import, diff and export of the exchange file. | @@ -401,7 +403,7 @@ in both repos (as separate PRs, one per repo). | 4d | Shared models | Client | MuMain | 4 | Models as entries of their own that items reference by name, so items can share a model (D23, shared models). | | 4e | Clear model loading errors | Client | MuMain | 4 | One message for missing model files and textures of item models that names the item, the model entry, the texture and the searched folders. | | 5 | Translation tooling | Client | MuMain | 2, 6 | Translations editor (items × languages), missing-translation warnings. The names themselves moved to phase 2 (D17). | -| 6 | Editors | Client | MuMain | 2–5 | Focused MuEditor tools (section 9), including add/remove items. | +| 6 | Editors | Client | MuMain | 2–5 | Focused MuEditor tools (section 9), including add/remove items; the looks of an item and the items that share a look (read only). | | 7 | Item sync, client side | Client | MuMain | 2, 6 | MuEditor import/export of the item exchange file, with diff. | | B | Item sync, server side | Server | OpenMU | 7 (file format) | Admin panel import/export pages for the item exchange file, with diff. | | 8 | Data-driven tooltips | Client | MuMain | 2, 5 | Tooltip JSON converted from the `ItemTooltip*` files; `RenderItemInfo()` reads data; tooltip editor. Moved earlier if needed. | @@ -411,7 +413,8 @@ in both repos (as separate PRs, one per repo). | 11 | Remaining item files | Both (per file) | MuMain, OpenMU as needed | 2 | `ItemAddOption`, `SocketItem`, `Mix`, `pet`, drop settings; one phase each, order decided later. | | 12 | New item groups *(to discuss again)* | Both | MuMain + D | 3, 4, 4d, A | Move items into new groups for the new client; level variants become items of their own; legacy ids for the original client (D21). | | D | Legacy item ids, server side *(to discuss again)* | Server | OpenMU | 12 | Legacy id on item definitions, mapping tool, Season 6 serializer sends legacy ids, serializer for the new client. | -| 13 | Cleanup | Client | MuMain | all | The glow of the Phoenix Soul inventory model, then remove this document. | +| 13 | Looks in data | Client | MuMain | 4c, 6 | What the named render styles and effects are made of moves from code into `Data/Effects/` as building blocks (D25), with the look editor in MuEditor (preview, bones, the items that use a look). | +| 14 | Cleanup | Client | MuMain | all | The glow of the Phoenix Soul inventory model, the follow-up design documents, then remove this document. | The deferred question Q1 (custom items on the original client) is a **Server** topic and must be decided before custom items are used on a @@ -583,8 +586,11 @@ server with original clients (after phases 6 and B). loaded their models); their recipe (textured, then `RENDER_BRIGHT | RENDER_CHROME2`) comes back as a style with their model entries. - - **4c3 Particle effects:** `effects` lists the particle effects of - `RenderPartObjectEffect` (about 80 item branches, 31 distinct). + - **4c3 Particle effects:** `effects` names the particle effects of + `RenderPartObjectEffect` (about 80 item branches, 31 distinct). Each + effect is named code, like the render styles; which values they + take (bones, colors, sizes) comes with phase 13, when what the named + looks are made of moves into data (D25). Verified like phase 3: one-time comparisons of the old and the new code (which files and texture folders are loaded for each model; the @@ -621,6 +627,10 @@ server with original clients (after phases 6 and B). a filter for missing translations) and missing-translation warnings. The names themselves are part of phase 2 (D17). 6. **Editors**: the MuEditor tools from section 9, including add/remove. + The model tool shows the looks of an item (glow, render style, effects) + and lists the items that share a look. It only reads the model data, + so it needs no change to the drawing code; the looks themselves are + edited from phase 13 on. 7. **Item sync, client side**: MuEditor import/export of the item exchange file and diff. @@ -740,7 +750,23 @@ server with original clients (after phases 6 and B). strength, command), the Orb of Summoning (which summon) and the Transformation Ring (which monster). The level chooses a kind, not a tier; to decide then. -13. **Cleanup**, once the work has landed: +13. **Looks in data** (D25): what the named render styles and effects are + made of moves from code into data files in `Data/Effects/`, as lists + of building blocks, one look at a time; the item data keeps naming + them. Particle and effect types are referenced by name and stay code + until their own design document. Effects place sprites and particles + on bones of the model, so they follow its animation. Bones are values + by number, as in the code: the names in the `.bmd` files are export + names that do not help to find a bone (of the 778 item model files, + 340 have the biped skeleton with names like "Bip01 L Hand", 249 only + names like `Bone01`, `Box02` or `zx12`, and in 396 the bone called + "BoneNN" is not bone NN); the look editor shows numbers and names + together, and the model loader checks that the bones exist. The lists + are resolved when loading (textures, bones, particle types), so + drawing is not slower. Verified like phase 4c: every look draws and + spawns the same things with the same values as its code did, per item + and level. The look editor of section 9 comes with it. +14. **Cleanup**, once the work has landed: - The glow of the inventory model of the Phoenix Soul Armor (`MODEL_ARMORINVEN_74`) moves into data. It is the only model drawn for an item that glows differently from its item: only on its first @@ -749,6 +775,15 @@ server with original clients (after phases 6 and B). other inventory models (probably a copy-paste leftover; to keep or to drop then). Left for last because it only shows in the inventory and is easiest to check once everything else is in data. + - Follow-up design documents for what these phases leave in code, + written before this one is removed: + - the particle and effect system of `ZzzEffect` as data: the particle + and effect types with their behavior and values, so looks + (phase 13) can use new ones without code; + - the looks of monsters and NPCs as data: their glow (still the + colors of the drawing code), render styles and effects, including + the plate look of the helper NPCs (`helperNpcPlate`, which belongs + to the NPCs, not to the items) and the player transformations. - Remove this document. ## Open questions From 6a447df9f58a7ad71629ce7729a4fac17b82376c Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Tue, 29 Sep 2026 22:24:21 +0200 Subject: [PATCH 2/9] docs(items): what belongs together beyond items, shared definitions (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. --- .../2026-09-25-data-driven-items-design.md | 111 ++++++++++++------ 1 file changed, 77 insertions(+), 34 deletions(-) diff --git a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md index 6892cf83b..6e0f1b4b9 100644 --- a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md +++ b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md @@ -55,7 +55,8 @@ pattern later but are not part of this work. | D22 | Model data files | Model and display data (model file, textures, inventory and ground display, cloth, effects) lives in separate files, `Data/Items/Models/GroupNN_*.json`, one per item group, items by `number`. It is client-only: the item files keep what client and server share, and only those take part in the OpenMU exchange. Separate files also keep the item files small and let the model editor and the stats editor change different files. | | D23 | Model slots | Two steps. **Own model slots (phase 4a–4c):** every item keeps its own model slot, `MODEL_ITEM + item type`, as today; the data only says which file is loaded into it, so the ~100 places that compute `+ MODEL_ITEM` / `- MODEL_ITEM` stay unchanged. **Shared models (phase 4d, right after them):** models become entries of their own, and items reference them by name, so several items can share one model (loaded once). The code then asks the item database for an item's model slot, and objects keep their item type instead of computing it back from the model. Phase 12 (level variants as items of their own) then shares models without loading them twice. | | D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and a list of particle effects (`RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. The named looks are code for now; D25 moves what they are made of into data. | -| D25 | Looks in data | Items keep referencing their looks by name (the render style and the effects; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/` (phase 13): a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; the item data written in phase 4 stays valid. A named look is shared: changing it changes every item that uses it, so the editor lists the items of a look and warns with that list before a change to a shared look is saved. The particle and effect types themselves (the particle system of `ZzzEffect`) become data as well, as a project with its own design document (phase 14). | +| D25 | Looks in data | Items keep referencing their looks by name (the render style and the effects; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/` (phase 13): a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; the item data written in phase 4 stays valid. A named look is shared: changing it changes every item that uses it, so the editor lists the items of a look and warns with that list before a change to a shared look is saved. Skills, monsters and NPCs use the same look format and the same look editor (see "Beyond items"); effect and particle types are referenced by the names of the effect catalogue (FX1), and become data themselves later (FX2). | +| D26 | Shared definitions | Anything that several things use is defined once, with a name, in a data file, and its users reference that name: glow colors, looks, effect types, skills. Editors show where a definition is used (items, skills, monsters, NPCs, other effects). Saving a change to a definition that others use first shows the list of those users; a copy makes a variant for one user; renaming updates all references; a definition that is still used cannot be deleted (the list shows why). Unknown names are reported when loading. Names are resolved when loading, so drawing and game logic never look names up. Every move from code into data is compared with the old code (recorder) before the old code goes. | | D21 | New item groups | *To discuss again when we reach phase 12.* Items may move into new groups (e.g. 16 = jewels, 17 = orbs) for the new client, while original Season 6 clients keep the old ids. Moved items keep their original id as a legacy id; OpenMU's Season 6 item serializer sends the legacy id, a serializer for the new client sends the new id. Planned after phases 3, 4 and OpenMU PR A, when little code depends on group numbers any more. | ## Current state @@ -403,7 +404,7 @@ in both repos (as separate PRs, one per repo). | 4d | Shared models | Client | MuMain | 4 | Models as entries of their own that items reference by name, so items can share a model (D23, shared models). | | 4e | Clear model loading errors | Client | MuMain | 4 | One message for missing model files and textures of item models that names the item, the model entry, the texture and the searched folders. | | 5 | Translation tooling | Client | MuMain | 2, 6 | Translations editor (items × languages), missing-translation warnings. The names themselves moved to phase 2 (D17). | -| 6 | Editors | Client | MuMain | 2–5 | Focused MuEditor tools (section 9), including add/remove items; the looks of an item and the items that share a look (read only). | +| 6 | Editors | Client | MuMain | 2–5 | Focused MuEditor tools (section 9) on the rules of D26, including add/remove items and picking the looks of an item from lists. | | 7 | Item sync, client side | Client | MuMain | 2, 6 | MuEditor import/export of the item exchange file, with diff. | | B | Item sync, server side | Server | OpenMU | 7 (file format) | Admin panel import/export pages for the item exchange file, with diff. | | 8 | Data-driven tooltips | Client | MuMain | 2, 5 | Tooltip JSON converted from the `ItemTooltip*` files; `RenderItemInfo()` reads data; tooltip editor. Moved earlier if needed. | @@ -413,8 +414,13 @@ in both repos (as separate PRs, one per repo). | 11 | Remaining item files | Both (per file) | MuMain, OpenMU as needed | 2 | `ItemAddOption`, `SocketItem`, `Mix`, `pet`, drop settings; one phase each, order decided later. | | 12 | New item groups *(to discuss again)* | Both | MuMain + D | 3, 4, 4d, A | Move items into new groups for the new client; level variants become items of their own; legacy ids for the original client (D21). | | D | Legacy item ids, server side *(to discuss again)* | Server | OpenMU | 12 | Legacy id on item definitions, mapping tool, Season 6 serializer sends legacy ids, serializer for the new client. | -| 13 | Looks in data | Client | MuMain | 4c, 6 | What the named render styles and effects are made of moves from code into `Data/Effects/` as building blocks (D25), with the look editor in MuEditor (preview, bones, the items that use a look). | -| 14 | Cleanup | Client | MuMain | all | The glow of the Phoenix Soul inventory model, the follow-up design documents, then remove this document. | +| FX1 | Effect catalogue *(own design document)* | Client | MuMain | 4c | Every effect, particle, lightning and sprite type gets a name and its creation values in `Data/Effects/`; behavior stays code. Effect browser with preview and "used by". See "Beyond items". | +| 13 | Looks in data | Client | MuMain | 4c, 6, FX1 | What the named render styles and effects are made of moves from code into `Data/Effects/` as building blocks (D25), with the look editor in MuEditor (preview, bones, the items that use a look). | +| SK1 | Skills as data *(own design document)* | Client, server sync later | MuMain | 2 | `Skill.bmd` into JSON like the items in phase 2; focused skill editors. | +| SK2 | Skill looks *(own design document)* | Client | MuMain | SK1, FX1, 13 | How skills look when cast, flying and hitting, as looks (D25); preview with a test character. | +| MN | Monsters and NPCs *(own design document)* | Client, server sync later | MuMain | FX1, 13, SK1 | Monster and NPC setup, looks and skills into data; monster and NPC editors. | +| FX2 | Effect behavior as data *(own design document)* | Client | MuMain | FX1 | How effects and particles move, fade, spawn and draw, as building blocks; effect editor with live preview. | +| 14 | Cleanup | Client | MuMain | all item phases | The glow of the Phoenix Soul inventory model, the design documents of the areas beyond items that do not have one yet, then remove this document. | The deferred question Q1 (custom items on the original client) is a **Server** topic and must be decided before custom items are used on a @@ -560,8 +566,8 @@ server with original clients (after phases 6 and B). and capes, or on one mesh). It replaces `PartObjectColor`, `PartObjectColor2`, `PartObjectColor3`, the mesh choices of `RenderPartObjectBodyColor(2)` and the item cases of the glow level - switch. Still in code: the glow of monsters (a follow-up of its own: - monsters have their own models, data and code), of the event models + switch. Still in code: the glow of monsters (area MN: monsters have + their own models, data and code), of the event models of level variants (with 4d and phase 12, when they get model entries and the drawing code knows the item), of the inventory model of the Phoenix Soul Armor (see the last step below), and the Deadly Staff's @@ -590,7 +596,11 @@ server with original clients (after phases 6 and B). `RenderPartObjectEffect` (about 80 item branches, 31 distinct). Each effect is named code, like the render styles; which values they take (bones, colors, sizes) comes with phase 13, when what the named - looks are made of moves into data (D25). + looks are made of moves into data (D25). MuEditor gets a read-only + view of the looks: for the selected item its model file, glow, + render style and effects, and for a look all items that use it + (D26). It only reads the model data, so it needs no change to the + drawing code. Verified like phase 3: one-time comparisons of the old and the new code (which files and texture folders are loaded for each model; the @@ -626,11 +636,11 @@ server with original clients (after phases 6 and B). 5. **Translation tooling**: a translations editor (items × languages, with a filter for missing translations) and missing-translation warnings. The names themselves are part of phase 2 (D17). -6. **Editors**: the MuEditor tools from section 9, including add/remove. - The model tool shows the looks of an item (glow, render style, effects) - and lists the items that share a look. It only reads the model data, - so it needs no change to the drawing code; the looks themselves are - edited from phase 13 on. +6. **Editors**: the MuEditor tools from section 9, including add/remove, + on the rules of D26 (one selection, "used by", a warning with the list + of users before a shared definition changes, copies for variants). The + model tool builds on the read-only look view of 4c3 and picks the looks + of an item from lists; the looks themselves are edited from phase 13 on. 7. **Item sync, client side**: MuEditor import/export of the item exchange file and diff. @@ -751,21 +761,21 @@ server with original clients (after phases 6 and B). Transformation Ring (which monster). The level chooses a kind, not a tier; to decide then. 13. **Looks in data** (D25): what the named render styles and effects are - made of moves from code into data files in `Data/Effects/`, as lists - of building blocks, one look at a time; the item data keeps naming - them. Particle and effect types are referenced by name and stay code - until their own design document. Effects place sprites and particles - on bones of the model, so they follow its animation. Bones are values - by number, as in the code: the names in the `.bmd` files are export - names that do not help to find a bone (of the 778 item model files, - 340 have the biped skeleton with names like "Bip01 L Hand", 249 only - names like `Bone01`, `Box02` or `zx12`, and in 396 the bone called + made of moves from code into data files in `Data/Effects/`, as lists of + building blocks, one look at a time; the item data keeps naming them. + Particle and effect types are referenced by their names from the effect + catalogue (FX1) and stay code until FX2. Effects place sprites and + particles on bones of the model, so they follow its animation. Bones are + values by number, as in the code: the names in the `.bmd` files are + export names that do not help to find a bone (of the 778 item model + files, 340 have the biped skeleton with names like "Bip01 L Hand", 249 + only names like `Bone01`, `Box02` or `zx12`, and in 396 the bone called "BoneNN" is not bone NN); the look editor shows numbers and names together, and the model loader checks that the bones exist. The lists - are resolved when loading (textures, bones, particle types), so - drawing is not slower. Verified like phase 4c: every look draws and - spawns the same things with the same values as its code did, per item - and level. The look editor of section 9 comes with it. + are resolved when loading (textures, bones, particle types), so drawing + is not slower. Verified like phase 4c: every look draws and spawns the + same things with the same values as its code did, per item and level. + The look editor of section 9 comes with it. 14. **Cleanup**, once the work has landed: - The glow of the inventory model of the Phoenix Soul Armor (`MODEL_ARMORINVEN_74`) moves into data. It is the only model drawn @@ -775,17 +785,50 @@ server with original clients (after phases 6 and B). other inventory models (probably a copy-paste leftover; to keep or to drop then). Left for last because it only shows in the inventory and is easiest to check once everything else is in data. - - Follow-up design documents for what these phases leave in code, - written before this one is removed: - - the particle and effect system of `ZzzEffect` as data: the particle - and effect types with their behavior and values, so looks - (phase 13) can use new ones without code; - - the looks of monsters and NPCs as data: their glow (still the - colors of the drawing code), render styles and effects, including - the plate look of the helper NPCs (`helperNpcPlate`, which belongs - to the NPCs, not to the items) and the player transformations. + - The design documents of the areas beyond items (next section) that + do not have one yet, written before this one is removed, so the plan + for them is not lost. - Remove this document. +## Beyond items: effects, skills, monsters and NPCs + +Items are not the only users of the effect code. About 3,950 calls create +effects, particles, lightning and sprites: about 1,600 for the maps and +their monsters (`World/GameMaps`), 1,100 for characters, objects and items +(`Engine`), 760 inside the effects themselves, 290 for skills, combat, +pets and events (`GameLogic`) and 140 for skill results from the server +(`Network`). The effect code knows about 440 effect types, 100 particle +types and 30 lightning types; `Render/Effects/EffectRegistry` has started +to describe them as a table instead of three large switches. Skills are +still read from `Skill.bmd` (with a table editor, as items had before +phase 2), and monsters and NPCs are set up in a switch of 403 cases in +`ZzzCharacter.cpp`. + +What belongs together is refactored together, on the rules of D26, in +this order. Each area gets its own design document and its own phases: + +| Area | Name | Depends on | What moves into data | Editor | +|---|---|---|---|---| +| FX1 | Effect catalogue | 4c | Every effect, particle, lightning and sprite type gets a name and its creation values (the `CreateParams` of the registry) in `Data/Effects/`. Behavior stays code. Items, skills and monsters then name effects instead of using type numbers. | Effect browser: each effect with a preview, its values, and where it is used. | +| 13 | Looks in data (items) | FX1, 6 | What the item looks are made of (D25). | Look editor (section 9). | +| SK1 | Skills as data | 2 | `Skill.bmd` into JSON, like the items in phase 2: names, requirements, rules; later synced with OpenMU like the items (phases 7 and B). | Focused skill editors, like section 9 for items; "used by" (classes, items that give a skill, monsters). | +| SK2 | Skill looks | SK1, FX1, 13 | How a skill looks when cast, flying and hitting (effects, particles, sounds, character animations), as looks in the format of D25. Skills place effects on bones too, mostly of the caster (hands, weapon), whose biped skeleton has readable bone names ("Bip01 R Hand"). | The look editor with its bone picking, with a preview of the skill cast by a test character. | +| MN | Monsters and NPCs | FX1, 13, SK1 | The monster and NPC setup (model, size, the equipment of player-shaped NPCs), their looks (glow, render styles and effects in the format of D25; the plate of the helper NPCs, `helperNpcPlate`, moves from the items to them; the player transformations) and their skills. | Monster and NPC editors with preview; the look editor. | +| FX2 | Effect behavior as data | FX1 | How effects and particles move, fade, spawn and draw, as building blocks, one effect family at a time (continuing the handlers of the registry). New effects without code. | Effect editor with a live preview. | +| later | Map objects, buffs, pets, sounds | FX1 | To decide: the objects of the maps with their effects (most of `World/GameMaps`), buff visuals, pets, sound names. | – | + +- **The effect catalogue comes first.** Its names are what the looks of + items (13), skills (SK2) and monsters (MN) reference. It moves no + behavior, so it is small and can be checked like the render styles. +- **One look format and one look editor** for items, skills, monsters and + NPCs. A look made for an item can be used by a monster, and the "used + by" list of a look shows all of them. +- **Effect behavior comes last.** Once everything references effects by + name, their behavior can move into data without touching their users. +- **The editors share their parts:** the look editor, the effect editor + and the bone viewer are opened from the item, skill and monster editors, + and all of them follow D26. + ## Open questions - **Q1: Custom items on the original client** *(deferred; must be From ba8dba03a72b3ebde3a4782f1b96987ba1e521e4 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Tue, 29 Sep 2026 22:48:16 +0200 Subject: [PATCH 3/9] docs: roadmap for data-driven content beyond items 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. --- .../2026-09-25-data-driven-items-design.md | 96 +++++---------- ...9-29-data-driven-content-roadmap-design.md | 112 ++++++++++++++++++ 2 files changed, 142 insertions(+), 66 deletions(-) create mode 100644 docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md diff --git a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md index 6e0f1b4b9..c34d55fab 100644 --- a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md +++ b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md @@ -26,8 +26,10 @@ code**: - Item names are **translatable**; logs always use a stable, language-neutral identifier. -Scope: **items only.** Skills, monsters, maps, etc. may follow the same -pattern later but are not part of this work. +Scope: **items only.** What goes beyond items (effects and particles, +skills, monsters and NPCs, and the look format and editing rules they +share with items) is planned in the +[roadmap](2026-09-29-data-driven-content-roadmap-design.md). ## Decisions so far @@ -55,8 +57,8 @@ pattern later but are not part of this work. | D22 | Model data files | Model and display data (model file, textures, inventory and ground display, cloth, effects) lives in separate files, `Data/Items/Models/GroupNN_*.json`, one per item group, items by `number`. It is client-only: the item files keep what client and server share, and only those take part in the OpenMU exchange. Separate files also keep the item files small and let the model editor and the stats editor change different files. | | D23 | Model slots | Two steps. **Own model slots (phase 4a–4c):** every item keeps its own model slot, `MODEL_ITEM + item type`, as today; the data only says which file is loaded into it, so the ~100 places that compute `+ MODEL_ITEM` / `- MODEL_ITEM` stay unchanged. **Shared models (phase 4d, right after them):** models become entries of their own, and items reference them by name, so several items can share one model (loaded once). The code then asks the item database for an item's model slot, and objects keep their item type instead of computing it back from the model. Phase 12 (level variants as items of their own) then shares models without loading them twice. | | D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and a list of particle effects (`RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. The named looks are code for now; D25 moves what they are made of into data. | -| D25 | Looks in data | Items keep referencing their looks by name (the render style and the effects; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/` (phase 13): a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; the item data written in phase 4 stays valid. A named look is shared: changing it changes every item that uses it, so the editor lists the items of a look and warns with that list before a change to a shared look is saved. Skills, monsters and NPCs use the same look format and the same look editor (see "Beyond items"); effect and particle types are referenced by the names of the effect catalogue (FX1), and become data themselves later (FX2). | -| D26 | Shared definitions | Anything that several things use is defined once, with a name, in a data file, and its users reference that name: glow colors, looks, effect types, skills. Editors show where a definition is used (items, skills, monsters, NPCs, other effects). Saving a change to a definition that others use first shows the list of those users; a copy makes a variant for one user; renaming updates all references; a definition that is still used cannot be deleted (the list shows why). Unknown names are reported when loading. Names are resolved when loading, so drawing and game logic never look names up. Every move from code into data is compared with the old code (recorder) before the old code goes. | +| D25 | Looks in data | Items keep naming their looks; what a named look is made of moves from code into data (phase 13). Skills, monsters and NPCs use the same look format, so the decision lives in the [roadmap](2026-09-29-data-driven-content-roadmap-design.md). | +| D26 | Shared definitions | Defined once with a name and referenced by it; editors show where a definition is used and warn with that list before a shared definition changes. The decision lives in the [roadmap](2026-09-29-data-driven-content-roadmap-design.md). | | D21 | New item groups | *To discuss again when we reach phase 12.* Items may move into new groups (e.g. 16 = jewels, 17 = orbs) for the new client, while original Season 6 clients keep the old ids. Moved items keep their original id as a legacy id; OpenMU's Season 6 item serializer sends the legacy id, a serializer for the new client sends the new id. Planned after phases 3, 4 and OpenMU PR A, when little code depends on group numbers any more. | ## Current state @@ -381,7 +383,7 @@ table: | **Rules** | Flags as a matrix (items × tradable, droppable, storable, sellable, …) for bulk editing. | | **Categories** | Tag-centered view: pick a tag, see and change its items. | | **Model and visuals** | Model file picker for `Data\Item\*.bmd` with 3D preview, texture folder, inventory and ground display values, cloth flag, and glow, render style and effects chosen from lists (D24), each with the list of the other items that use the same look. | -| **Look editor** (phase 13) | Edits a named look (D25) as its list of building blocks, with a live preview of the model and its animations. Shows the skeleton with every bone's number and its name from the `.bmd` file, highlights the bones the look uses, and picks bones by clicking them instead of looking up numbers in the model file. Lists all items that use the look; saving a change to a look that other items share shows a warning with the list of those items. A new look starts as a copy of another. | +| **Look editor** (phase 13) | The shared look editor of the [roadmap](2026-09-29-data-driven-content-roadmap-design.md), opened for the looks of the selected item: its building blocks with a live preview, bone picking, the items that use a look and the warning before a shared look changes. | | **Translations** | Items × languages grid, filter for missing translations. | | **OpenMU sync** | Import, diff and export of the exchange file. | @@ -414,13 +416,9 @@ in both repos (as separate PRs, one per repo). | 11 | Remaining item files | Both (per file) | MuMain, OpenMU as needed | 2 | `ItemAddOption`, `SocketItem`, `Mix`, `pet`, drop settings; one phase each, order decided later. | | 12 | New item groups *(to discuss again)* | Both | MuMain + D | 3, 4, 4d, A | Move items into new groups for the new client; level variants become items of their own; legacy ids for the original client (D21). | | D | Legacy item ids, server side *(to discuss again)* | Server | OpenMU | 12 | Legacy id on item definitions, mapping tool, Season 6 serializer sends legacy ids, serializer for the new client. | -| FX1 | Effect catalogue *(own design document)* | Client | MuMain | 4c | Every effect, particle, lightning and sprite type gets a name and its creation values in `Data/Effects/`; behavior stays code. Effect browser with preview and "used by". See "Beyond items". | +| FX1 | Effect catalogue *([roadmap](2026-09-29-data-driven-content-roadmap-design.md))* | Client | MuMain | 4c | Every effect, particle, lightning and sprite type gets a name and its creation values in `Data/Effects/`; behavior stays code. | | 13 | Looks in data | Client | MuMain | 4c, 6, FX1 | What the named render styles and effects are made of moves from code into `Data/Effects/` as building blocks (D25), with the look editor in MuEditor (preview, bones, the items that use a look). | -| SK1 | Skills as data *(own design document)* | Client, server sync later | MuMain | 2 | `Skill.bmd` into JSON like the items in phase 2; focused skill editors. | -| SK2 | Skill looks *(own design document)* | Client | MuMain | SK1, FX1, 13 | How skills look when cast, flying and hitting, as looks (D25); preview with a test character. | -| MN | Monsters and NPCs *(own design document)* | Client, server sync later | MuMain | FX1, 13, SK1 | Monster and NPC setup, looks and skills into data; monster and NPC editors. | -| FX2 | Effect behavior as data *(own design document)* | Client | MuMain | FX1 | How effects and particles move, fade, spawn and draw, as building blocks; effect editor with live preview. | -| 14 | Cleanup | Client | MuMain | all item phases | The glow of the Phoenix Soul inventory model, the design documents of the areas beyond items that do not have one yet, then remove this document. | +| 14 | Cleanup | Client | MuMain | all item phases | The glow of the Phoenix Soul inventory model; what later areas still need moves to the roadmap; then remove this document. | The deferred question Q1 (custom items on the original client) is a **Server** topic and must be decided before custom items are used on a @@ -566,8 +564,8 @@ server with original clients (after phases 6 and B). and capes, or on one mesh). It replaces `PartObjectColor`, `PartObjectColor2`, `PartObjectColor3`, the mesh choices of `RenderPartObjectBodyColor(2)` and the item cases of the glow level - switch. Still in code: the glow of monsters (area MN: monsters have - their own models, data and code), of the event models + switch. Still in code: the glow of monsters (area MN of the roadmap: + monsters have their own models, data and code), of the event models of level variants (with 4d and phase 12, when they get model entries and the drawing code knows the item), of the inventory model of the Phoenix Soul Armor (see the last step below), and the Deadly Staff's @@ -764,18 +762,15 @@ server with original clients (after phases 6 and B). made of moves from code into data files in `Data/Effects/`, as lists of building blocks, one look at a time; the item data keeps naming them. Particle and effect types are referenced by their names from the effect - catalogue (FX1) and stay code until FX2. Effects place sprites and - particles on bones of the model, so they follow its animation. Bones are - values by number, as in the code: the names in the `.bmd` files are - export names that do not help to find a bone (of the 778 item model - files, 340 have the biped skeleton with names like "Bip01 L Hand", 249 - only names like `Bone01`, `Box02` or `zx12`, and in 396 the bone called - "BoneNN" is not bone NN); the look editor shows numbers and names - together, and the model loader checks that the bones exist. The lists - are resolved when loading (textures, bones, particle types), so drawing - is not slower. Verified like phase 4c: every look draws and spawns the - same things with the same values as its code did, per item and level. - The look editor of section 9 comes with it. + catalogue (FX1, roadmap) and stay code until FX2. Effects place sprites + and particles on bones of the model, so they follow its animation. Bones + are values by number, as in the code (see "Bones" in the roadmap for + why); the look editor shows numbers and names together, and the model + loader checks that the bones exist. The lists are resolved when loading + (textures, bones, particle types), so drawing is not slower. Verified + like phase 4c: every look draws and spawns the same things with the same + values as its code did, per item and level. The look editor of section 9 + comes with it. 14. **Cleanup**, once the work has landed: - The glow of the inventory model of the Phoenix Soul Armor (`MODEL_ARMORINVEN_74`) moves into data. It is the only model drawn @@ -785,49 +780,18 @@ server with original clients (after phases 6 and B). other inventory models (probably a copy-paste leftover; to keep or to drop then). Left for last because it only shows in the inventory and is easiest to check once everything else is in data. - - The design documents of the areas beyond items (next section) that - do not have one yet, written before this one is removed, so the plan - for them is not lost. + - Whatever later areas still need from this document moves to the + roadmap, so it is not lost. - Remove this document. -## Beyond items: effects, skills, monsters and NPCs - -Items are not the only users of the effect code. About 3,950 calls create -effects, particles, lightning and sprites: about 1,600 for the maps and -their monsters (`World/GameMaps`), 1,100 for characters, objects and items -(`Engine`), 760 inside the effects themselves, 290 for skills, combat, -pets and events (`GameLogic`) and 140 for skill results from the server -(`Network`). The effect code knows about 440 effect types, 100 particle -types and 30 lightning types; `Render/Effects/EffectRegistry` has started -to describe them as a table instead of three large switches. Skills are -still read from `Skill.bmd` (with a table editor, as items had before -phase 2), and monsters and NPCs are set up in a switch of 403 cases in -`ZzzCharacter.cpp`. - -What belongs together is refactored together, on the rules of D26, in -this order. Each area gets its own design document and its own phases: - -| Area | Name | Depends on | What moves into data | Editor | -|---|---|---|---|---| -| FX1 | Effect catalogue | 4c | Every effect, particle, lightning and sprite type gets a name and its creation values (the `CreateParams` of the registry) in `Data/Effects/`. Behavior stays code. Items, skills and monsters then name effects instead of using type numbers. | Effect browser: each effect with a preview, its values, and where it is used. | -| 13 | Looks in data (items) | FX1, 6 | What the item looks are made of (D25). | Look editor (section 9). | -| SK1 | Skills as data | 2 | `Skill.bmd` into JSON, like the items in phase 2: names, requirements, rules; later synced with OpenMU like the items (phases 7 and B). | Focused skill editors, like section 9 for items; "used by" (classes, items that give a skill, monsters). | -| SK2 | Skill looks | SK1, FX1, 13 | How a skill looks when cast, flying and hitting (effects, particles, sounds, character animations), as looks in the format of D25. Skills place effects on bones too, mostly of the caster (hands, weapon), whose biped skeleton has readable bone names ("Bip01 R Hand"). | The look editor with its bone picking, with a preview of the skill cast by a test character. | -| MN | Monsters and NPCs | FX1, 13, SK1 | The monster and NPC setup (model, size, the equipment of player-shaped NPCs), their looks (glow, render styles and effects in the format of D25; the plate of the helper NPCs, `helperNpcPlate`, moves from the items to them; the player transformations) and their skills. | Monster and NPC editors with preview; the look editor. | -| FX2 | Effect behavior as data | FX1 | How effects and particles move, fade, spawn and draw, as building blocks, one effect family at a time (continuing the handlers of the registry). New effects without code. | Effect editor with a live preview. | -| later | Map objects, buffs, pets, sounds | FX1 | To decide: the objects of the maps with their effects (most of `World/GameMaps`), buff visuals, pets, sound names. | – | - -- **The effect catalogue comes first.** Its names are what the looks of - items (13), skills (SK2) and monsters (MN) reference. It moves no - behavior, so it is small and can be checked like the render styles. -- **One look format and one look editor** for items, skills, monsters and - NPCs. A look made for an item can be used by a monster, and the "used - by" list of a look shows all of them. -- **Effect behavior comes last.** Once everything references effects by - name, their behavior can move into data without touching their users. -- **The editors share their parts:** the look editor, the effect editor - and the bone viewer are opened from the item, skill and monster editors, - and all of them follow D26. +## Beyond items + +Effects and particles, skills, monsters and NPCs are areas of the +[roadmap](2026-09-29-data-driven-content-roadmap-design.md), each with +its own design document when its work starts. They share the look format +(D25), the rules for shared definitions (D26) and the editor parts (look +editor, effect editor, bones) with the items. Item phase 13 depends on the +effect catalogue (FX1) there. ## Open questions diff --git a/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md b/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md new file mode 100644 index 000000000..fb6684708 --- /dev/null +++ b/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md @@ -0,0 +1,112 @@ +# Data-Driven Content Roadmap (WIP) + +> **Working document.** It keeps the order of the data-driven refactorings +> that go beyond one area, the decisions they share, and the editor parts +> they share. Each area has its own design document with its own phases; +> this document links them and stays while areas are open. +> +> Started from the items design +> ([2026-09-25-data-driven-items-design.md](2026-09-25-data-driven-items-design.md)), +> based on MuMain `upstream/main` @ `e4831483`. + +## Goal + +Content that is hardcoded in the client today (items and their looks, +effects and particles, skills, monsters and NPCs, ...) becomes data that +can be changed and edited in MuEditor without code. What belongs together +is planned together: items, skills and monsters all use the effect code, +so they share one way to describe looks and one set of editor parts. +Nothing gets slower in game: names and values are resolved when loading. + +## Why these areas belong together + +Items are not the only users of the effect code. About 3,950 calls create +effects, particles, lightning and sprites: about 1,600 for the maps and +their monsters (`World/GameMaps`), 1,100 for characters, objects and items +(`Engine`), 760 inside the effects themselves, 290 for skills, combat, +pets and events (`GameLogic`) and 140 for skill results from the server +(`Network`). The effect code knows about 440 effect types, 100 particle +types and 30 lightning types; `Render/Effects/EffectRegistry` has started +to describe them as a table instead of three large switches. Skills are +still read from `Skill.bmd` (with a table editor, as items had before +their phase 2), and monsters and NPCs are set up in a switch of 403 cases +in `ZzzCharacter.cpp`. + +## Shared decisions + +These decisions were made in the items design; they keep their numbers, +so the references there stay valid. + +| # | Topic | Decision | +|---|---|---| +| D25 | Looks in data | Items, skills, monsters and NPCs reference their looks by name (for items: the render style and the effects; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/`: a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; data that names looks stays valid when their definitions move. One look format and one look editor for all areas: a look made for an item can be used by a monster. Effect and particle types are referenced by the names of the effect catalogue (FX1) and become data themselves later (FX2). | +| D26 | Shared definitions | Anything that several things use is defined once, with a name, in a data file, and its users reference that name: glow colors, looks, effect types, skills. Editors show where a definition is used (items, skills, monsters, NPCs, other effects). Saving a change to a definition that others use first shows the list of those users; a copy makes a variant for one user; renaming updates all references; a definition that is still used cannot be deleted (the list shows why). Unknown names are reported when loading. Names are resolved when loading, so drawing and game logic never look names up. Every move from code into data is compared with the old code (recorder) before the old code goes. | + +## Bones + +Effects place sprites, particles and lightning on bones of a model, so +they follow its animation: the lights of a wing move as it flaps, the +casting effect of a skill sits in the caster's hand. Looks store bones as +numbers, as the code does, and the model loader checks that they exist. + +- **Item models:** the names in the `.bmd` files are export names that do + not help to find a bone. Of the 778 item model files, 340 have the + biped skeleton with names like "Bip01 L Hand", 249 only names like + `Bone01`, `Box02` or `zx12`, in 396 the bone called "BoneNN" is not bone + NN, and one file has a name twice. Some models have helper bones placed + as effect points (the 22 blue lights of the Wing of Eternal sit on the + bones `zx01` to `zx22`). +- **Characters:** skills place effects mostly on the caster's bones + (hands, weapon), and effects follow their owner's bones. Characters have + the biped skeleton, whose names are readable ("Bip01 R Hand"). + +The look editor shows numbers and names together and picks bones by +clicking them. + +## Areas and order + +| Area | Name | Design document | Depends on | What moves into data | Editor | +|---|---|---|---|---|---| +| Items | Items | [items design](2026-09-25-data-driven-items-design.md), phases 0–14 | – | Item data, rules and categories, models, glow, render styles and effects (as names). | The item tools of section 9 there. | +| FX1 | Effect catalogue | not yet | items 4c | Every effect, particle, lightning and sprite type gets a name and its creation values (the `CreateParams` of the registry) in `Data/Effects/`. Behavior stays code. Items, skills and monsters then name effects instead of using type numbers. | Effect browser: each effect with a preview, its values, and where it is used. | +| Items 13 | Looks in data (items) | items design, phase 13 | FX1, items 6 | What the item looks are made of (D25). | Look editor. | +| SK1 | Skills as data | not yet | items 2 (the same format rules) | `Skill.bmd` into JSON, like the items in their phase 2: names, requirements, rules; later synced with OpenMU like the items. | Focused skill editors, like the item tools; "used by" (classes, items that give a skill, monsters). | +| SK2 | Skill looks | not yet | SK1, FX1, items 13 | How a skill looks when cast, flying and hitting (effects, particles, sounds, character animations), as looks (D25), mostly on the caster's bones. | Look editor, with a preview of the skill cast by a test character. | +| MN | Monsters and NPCs | not yet | FX1, items 13, SK1 | The monster and NPC setup (model, size, the equipment of player-shaped NPCs), their looks (glow, render styles and effects as in D25; the plate of the helper NPCs, `helperNpcPlate`, moves from the items to them; the player transformations) and their skills. | Monster and NPC editors with preview; the look editor. | +| FX2 | Effect behavior as data | not yet | FX1 | How effects and particles move, fade, spawn and draw, as building blocks, one effect family at a time (continuing the handlers of the registry). New effects without code. | Effect editor with a live preview. | +| later | Map objects, buffs, pets, sounds | not yet | FX1 | To decide: the objects of the maps with their effects (most of `World/GameMaps`), buff visuals, pets, sound names. | – | + +- **The effect catalogue comes first.** Its names are what the looks of + items (items 13), skills (SK2) and monsters (MN) reference. It moves no + behavior, so it is small and can be checked like the render styles. +- **Skills before monsters,** because monsters use skills. +- **Effect behavior comes last.** Once everything references effects by + name, their behavior can move into data without touching their users. +- **Map objects later,** although the maps are the largest users of + effects: they only need the effect catalogue, not the looks of items, + skills or monsters. + +## Shared editor parts + +The editors of all areas follow D26: focused tools that share one +selection, "used by" lists, a warning with the list of users before a +shared definition changes, copies for variants. These parts are shared +and opened from the item, skill, monster and NPC editors: + +- **Look editor** (from items 13 on): edits a named look (D25) as its + list of building blocks, with a live preview of the model and its + animations (for skills: cast by a test character). Shows the skeleton + with every bone's number and name, highlights the bones the look uses, + and picks bones by clicking them. Lists all users of the look; saving a + change to a look that others use shows a warning with that list. A new + look starts as a copy of another. +- **Effect browser** (FX1) and **effect editor** (FX2): an effect alone in + a preview, its values, and where it is used. + +## When an area starts + +Its design document is written from the code first, like the items design +(current state, decisions, phases), linked in the table above, and this +document is updated. The default verification is the recorder method of +the items design: the old and the new code are run for every model, setup +and level, and what they draw and spawn is compared. From e83ec021c93e3a022e1295639cd4c10248c85b46 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Tue, 29 Sep 2026 23:35:31 +0200 Subject: [PATCH 4/9] feat(items): item effects from the model files (phase 4c3) 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. --- docs/item-data.md | 32 + .../2026-09-25-data-driven-items-design.md | 34 +- ...9-29-data-driven-content-roadmap-design.md | 2 +- src/Localization/Editor.en.resx | 24 + .../UI/ItemEditor/ItemEditorLooks.cpp | 85 +++ src/MuEditor/UI/ItemEditor/ItemEditorLooks.h | 14 + src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp | 4 + src/bin/Data/Items/Models/Group03_Spear.json | 3 +- src/bin/Data/Items/Models/Group05_Staff.json | 3 +- src/bin/Data/Items/Models/Group07_Helm.json | 6 +- src/bin/Data/Items/Models/Group09_Pants.json | 3 +- src/bin/Data/Items/Models/Group10_Gloves.json | 3 +- src/bin/Data/Items/Models/Group11_Boots.json | 3 +- src/bin/Data/Items/Models/Group12_Wing.json | 159 ++++- src/bin/Data/Items/Models/Group13_Helper.json | 42 +- src/bin/Data/Items/Models/Group14_Potion.json | 54 +- .../DataHandler/ItemData/ItemModelLoader.cpp | 20 +- .../DataHandler/ItemData/ItemModelLoader.h | 19 +- .../DataHandler/ItemData/ItemModelProblem.cpp | 4 +- .../DataHandler/ItemData/ItemModelProblem.h | 7 +- .../GameData/ItemData/ItemModelDefinition.h | 4 + .../GameData/ItemData/ItemModelJsonFormat.cpp | 24 +- src/source/Engine/Object/ZzzObject.cpp | 632 ++--------------- src/source/Engine/Object/ZzzOpenData.cpp | 3 +- src/source/Render/Items/ItemEffects.cpp | 661 ++++++++++++++++++ src/source/Render/Items/ItemEffects.h | 37 + src/source/Render/Items/ItemModelLookup.h | 46 ++ src/source/Render/Items/ItemRenderStyles.cpp | 49 +- tests/data/test_item_model_files.cpp | 105 ++- tests/data/test_item_model_json.cpp | 18 + tests/data/test_item_model_problems.cpp | 13 +- 31 files changed, 1418 insertions(+), 695 deletions(-) create mode 100644 src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp create mode 100644 src/MuEditor/UI/ItemEditor/ItemEditorLooks.h create mode 100644 src/source/Render/Items/ItemEffects.cpp create mode 100644 src/source/Render/Items/ItemEffects.h diff --git a/docs/item-data.md b/docs/item-data.md index 649646789..d3754fbae 100644 --- a/docs/item-data.md +++ b/docs/item-data.md @@ -246,6 +246,7 @@ item files hold what client and server share. | `ground` | How the item lies on the ground, see below. Optional. | | `glow` | How the item glows, see below. Optional. | | `renderStyle` | The look of the model when it is more than a plain textured model, see below. Optional. | +| `effect` | What the model does besides being drawn (sprites and particles on its bones, a pulsing glow mesh, ...), see below. Optional. | | `cloth` | `true` for capes that are worn as cloth: when one is put on or taken off, the character's cloth is deleted, so the next cape builds its own. Optional. The flag does not make a cape cloth; which capes are drawn as cloth, and how, is still decided in code. | `inventory` and `ground` hold these values; a missing value has the @@ -339,6 +340,36 @@ only in the hands of the Metal Balrog and the Orc Archer of Doom, `helperNpcPlate` only on the helper NPCs (Luke and Leo the Helper, Helper Ellen; in the code the flag of a PC room look, which no player gets). +`effect` names what the model does besides being drawn: + +```json +{ "number": 37, "file": "Data/Item/wing09.bmd", "textureFolders": ["Item"], "effect": "wingOfEternal" } +``` + +Each effect is code with a name; the names are listed at the end of +`src/source/Render/Items/ItemEffects.cpp`. An effect runs before the model +is drawn and can: + +- place sprites, particles and lightning on bones of the model, so they + follow its animation (the Devil's Key and Invitation, Rena, the wings of + the third tier, the Wings of Darkness); +- 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); +- draw the model itself instead of the usual drawing (the Dark Lord's + scrolls, `fruits`, `spirit`, `bloodBone`, `invisibilityCloak`, + `firecracker`, `gmGift`, `meshesPerLevel`). + +Three effects instead give items their own shine below +3: +`harmonyShine` (Jewel of Harmony, Moonstone Pendant, Illusion Sorcerer +Covenant), `sealShine` (the seals) and `cursedCastleWater`. Items with the +same effect share it. The socket seeds and spheres and zen have no effect; +they glow like level 0 (`"glow": {"level": 0}`), whatever their level. + +The effects of the event models that level variants are drawn with stay in +code; they get model entries later. + - All item models are loaded at startup, on the loading screen. - An item without a model entry is not drawn. Some items are drawn with the model of another item or with an effect model; that choice, and @@ -383,6 +414,7 @@ The problems are: | `noneBlendMeshes` has a mesh number the model does not have. | warning | | A `glow` value names a mesh the model does not have; that glow is not drawn (a hidden mesh: the glow is on all meshes). | warning | | `renderStyle` names a style that does not exist; the model is drawn plainly. | warning | +| `effect` names an effect that does not exist; the model is drawn without it. | warning | The model names textures as `.jpg`/`.tga`; the game reads the encrypted copies with the same name, `.OZJ`/`.OZT`. Meshes whose texture name starts diff --git a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md index c34d55fab..9dc337abf 100644 --- a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md +++ b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md @@ -56,7 +56,7 @@ share with items) is planned in the | D19 | Editor sync | Every item editor change goes into the database right away (phase 2), so the editor and the database never differ. Moving the editor fully onto the database stays in phase 6. | | D22 | Model data files | Model and display data (model file, textures, inventory and ground display, cloth, effects) lives in separate files, `Data/Items/Models/GroupNN_*.json`, one per item group, items by `number`. It is client-only: the item files keep what client and server share, and only those take part in the OpenMU exchange. Separate files also keep the item files small and let the model editor and the stats editor change different files. | | D23 | Model slots | Two steps. **Own model slots (phase 4a–4c):** every item keeps its own model slot, `MODEL_ITEM + item type`, as today; the data only says which file is loaded into it, so the ~100 places that compute `+ MODEL_ITEM` / `- MODEL_ITEM` stay unchanged. **Shared models (phase 4d, right after them):** models become entries of their own, and items reference them by name, so several items can share one model (loaded once). The code then asks the item database for an item's model slot, and objects keep their item type instead of computing it back from the model. Phase 12 (level variants as items of their own) then shares models without loading them twice. | -| D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and a list of particle effects (`RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. The named looks are code for now; D25 moves what they are made of into data. | +| D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and an effect (the item branches of `RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. The named looks are code for now; D25 moves what they are made of into data. | | D25 | Looks in data | Items keep naming their looks; what a named look is made of moves from code into data (phase 13). Skills, monsters and NPCs use the same look format, so the decision lives in the [roadmap](2026-09-29-data-driven-content-roadmap-design.md). | | D26 | Shared definitions | Defined once with a name and referenced by it; editors show where a definition is used and warn with that list before a shared definition changes. The decision lives in the [roadmap](2026-09-29-data-driven-content-roadmap-design.md). | | D21 | New item groups | *To discuss again when we reach phase 12.* Items may move into new groups (e.g. 16 = jewels, 17 = orbs) for the new client, while original Season 6 clients keep the old ids. Moved items keep their original id as a legacy id; OpenMU's Season 6 item serializer sends the legacy id, a serializer for the new client sends the new id. Planned after phases 3, 4 and OpenMU PR A, when little code depends on group numbers any more. | @@ -402,7 +402,7 @@ in both repos (as separate PRs, one per repo). | 2 | Data file format and names | Client | MuMain | 1 | JSON per group becomes the only item source and the database becomes the source for `ItemAttribute[]`; translated names in the UI locale; loading, writing and validation rules; automated data test; bmd import (with repair) and export in MuEditor; editor edits go into the database right away. | | 3 | Rules and categories into data | Client | MuMain | 2 | Flags and tags replace the hardcoded lists; client ↔ OpenMU rule mapping (input for A). | | A | Server rule fields and checks | Server | OpenMU | 3 (mapping) | New `ItemDefinition` fields or tables, migration, Season 6 values, update plug-in, enforcement in player actions. | -| 4 | Models into data | Client | MuMain | 2 | Model files per item group (D22), model slots stay `MODEL_ITEM + type` (D23, own model slots). One PR per part: **4a** model files and textures (`OpenItems()` / `OpenItemTextures()`), **4b** inventory and ground display, **4c** render effects (D24) in three parts: **4c1** glow, **4c2** render styles, **4c3** particle effects. | +| 4 | Models into data | Client | MuMain | 2 | Model files per item group (D22), model slots stay `MODEL_ITEM + type` (D23, own model slots). One PR per part: **4a** model files and textures (`OpenItems()` / `OpenItemTextures()`), **4b** inventory and ground display, **4c** render effects (D24) in three parts: **4c1** glow, **4c2** render styles, **4c3** item effects. | | 4d | Shared models | Client | MuMain | 4 | Models as entries of their own that items reference by name, so items can share a model (D23, shared models). | | 4e | Clear model loading errors | Client | MuMain | 4 | One message for missing model files and textures of item models that names the item, the model entry, the texture and the searched folders. | | 5 | Translation tooling | Client | MuMain | 2, 6 | Translations editor (items × languages), missing-translation warnings. The names themselves moved to phase 2 (D17). | @@ -532,7 +532,7 @@ server with original clients (after phases 6 and B). "inventory": { "offset": [-0.02, 0.03], "rotation": [180, 270, 15], "scale": 0.0039 }, "ground": { "rotation": [60, 0, -45], "scale": 1.0 }, "glow": { "color": "gold", "meshes": [0] }, - "renderStyle": "chromeMesh0", "effects": ["flameSparks"] } + "renderStyle": "chromeMesh0", "effect": "flameSparks" } ``` Every item keeps its model slot `MODEL_ITEM + item type` (D23, own model slots). Three @@ -590,15 +590,25 @@ server with original clients (after phases 6 and B). loaded their models); their recipe (textured, then `RENDER_BRIGHT | RENDER_CHROME2`) comes back as a style with their model entries. - - **4c3 Particle effects:** `effects` names the particle effects of - `RenderPartObjectEffect` (about 80 item branches, 31 distinct). Each - effect is named code, like the render styles; which values they - take (bones, colors, sizes) comes with phase 13, when what the named - looks are made of moves into data (D25). MuEditor gets a read-only - view of the looks: for the selected item its model file, glow, - render style and effects, and for a look all items that use it - (D26). It only reads the model data, so it needs no change to the - drawing code. + - **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 + `Render/Items/ItemEffects.cpp` (moved unchanged; the Siege Potion and + the Contract share one), and the shine of 10 items below +3 becomes + three more (`harmonyShine`, `sealShine`, `cursedCastleWater`). An + effect places sprites, particles and lightning on bones, 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 socket + seeds and spheres and zen need no effect: they glow like level 0 + (`"glow": {"level": 0}`), which is what their level checks did. The + effects of the event models of level variants stay in + `RenderPartObjectEffect` (no model entries yet). Which values the + effects take (bones, colors, sizes) comes with phase 13, when what + the named looks are made of moves into data (D25). MuEditor gets a + read-only view of the looks above the item table: for the selected + item its model file, glow, render style and effect, and for a look + all items that use it (D26). It only reads the model data, so it + needs no change to the drawing code. Verified like phase 3: one-time comparisons of the old and the new code (which files and texture folders are loaded for each model; the diff --git a/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md b/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md index fb6684708..b5cf0aa3d 100644 --- a/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md +++ b/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md @@ -39,7 +39,7 @@ so the references there stay valid. | # | Topic | Decision | |---|---|---| -| D25 | Looks in data | Items, skills, monsters and NPCs reference their looks by name (for items: the render style and the effects; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/`: a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; data that names looks stays valid when their definitions move. One look format and one look editor for all areas: a look made for an item can be used by a monster. Effect and particle types are referenced by the names of the effect catalogue (FX1) and become data themselves later (FX2). | +| D25 | Looks in data | Items, skills, monsters and NPCs reference their looks by name (for items: the render style and the effect; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/`: a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; data that names looks stays valid when their definitions move. One look format and one look editor for all areas: a look made for an item can be used by a monster. Effect and particle types are referenced by the names of the effect catalogue (FX1) and become data themselves later (FX2). | | D26 | Shared definitions | Anything that several things use is defined once, with a name, in a data file, and its users reference that name: glow colors, looks, effect types, skills. Editors show where a definition is used (items, skills, monsters, NPCs, other effects). Saving a change to a definition that others use first shows the list of those users; a copy makes a variant for one user; renaming updates all references; a definition that is still used cannot be deleted (the list shows why). Unknown names are reported when loading. Names are resolved when loading, so drawing and game logic never look names up. Every move from code into data is compared with the old code (recorder) before the old code goes. | ## Bones diff --git a/src/Localization/Editor.en.resx b/src/Localization/Editor.en.resx index e8b6d4efb..ce5346046 100644 --- a/src/Localization/Editor.en.resx +++ b/src/Localization/Editor.en.resx @@ -463,4 +463,28 @@ The item data files could not be written. See the console. + + Looks + + + Model file + + + Glow + + + Render style + + + Effect + + + used by + + + Select an item in the table to see its looks. + + + The selected item has no model. + diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp new file mode 100644 index 000000000..20624a537 --- /dev/null +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp @@ -0,0 +1,85 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "ItemEditorLooks.h" +#include "ItemEditorTable.h" + +#include "Data/GameData/ItemData/ItemDatabase.h" +#include "Data/GameData/ItemData/ItemModelDatabase.h" +#include "Data/GameData/ItemData/ItemModelGlowJson.h" +#include "I18N/All.h" +#include "imgui.h" + +#include +#include + +namespace +{ +using Data::Items::ItemModelDefinition; +using LookName = std::string ItemModelDefinition::*; + +// The glow values as the model file has them, or "-" without any. +std::string DescribeGlow(const ItemModelDefinition& model) +{ + Data::Items::Json::OrderedJson json = Data::Items::Json::OrderedJson::object(); + Data::Items::GlowJson::Write(model, json); + const auto glow = json.find(Data::Items::GlowJson::GlowKey); + return glow != json.end() ? glow->dump() : "-"; +} + +// A look of the item with the number of items that use it; opened, the list +// of those items. Clicking one selects it in the table. +void RenderLook(const char* label, const ItemModelDefinition& model, LookName look) +{ + const std::string& name = model.*look; + if (name.empty()) + { + ImGui::Text("%s: -", label); + return; + } + + const std::span models = g_ItemModelDatabase.GetAllSlots(); + const auto usesLook = [&](const ItemModelDefinition& other) { return other.Exists() && other.*look == name; }; + int users = 0; + for (const ItemModelDefinition& other : models) + { + users += usesLook(other) ? 1 : 0; + } + + ImGui::PushID(label); + if (ImGui::TreeNode("look", "%s: %s (%s %d)", label, name.c_str(), I18N::Editor::UsedBy, users)) + { + for (int itemType = 0; itemType < static_cast(models.size()); ++itemType) + { + if (usesLook(models[itemType]) && ImGui::Selectable(g_ItemDatabase.GetLogName(itemType).c_str(), false)) + { + CItemEditorTable::RequestScrollToIndex(itemType); + } + } + ImGui::TreePop(); + } + ImGui::PopID(); +} +} // namespace + +void CItemEditorLooks::Render(int itemType) +{ + if (!ImGui::CollapsingHeader(I18N::Editor::Looks)) + { + return; + } + + const ItemModelDefinition* model = g_ItemModelDatabase.Find(itemType); + if (model == nullptr) + { + ImGui::TextDisabled("%s", itemType < 0 ? I18N::Editor::SelectAnItem : I18N::Editor::NoModel); + return; + } + ImGui::Text("%s: %s", I18N::Editor::ModelFile, model->file.c_str()); + ImGui::Text("%s: %s", I18N::Editor::Glow, DescribeGlow(*model).c_str()); + RenderLook(I18N::Editor::RenderStyle, *model, &ItemModelDefinition::renderStyle); + RenderLook(I18N::Editor::Effect, *model, &ItemModelDefinition::effect); +} + +#endif // _EDITOR diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h new file mode 100644 index 000000000..394995ef7 --- /dev/null +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h @@ -0,0 +1,14 @@ +#pragma once + +#ifdef _EDITOR + +// A read-only view of the looks of the selected item (Data/Items/Models): its +// model file, glow, render style and effect, and the items that share a look. +class CItemEditorLooks +{ +public: + // Shown above the item table; `itemType` is the selected item, -1 for none. + static void Render(int itemType); +}; + +#endif // _EDITOR diff --git a/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp b/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp index 9a9ca9bd0..f5faa1617 100644 --- a/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp +++ b/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp @@ -5,6 +5,7 @@ #include "MuItemEditorUI.h" #include "ItemEditorTable.h" #include "ItemEditorActions.h" +#include "ItemEditorLooks.h" #include "ItemEditorPopups.h" #include "Data/GameData/ItemData/ItemFieldMetadata.h" #include "../MuEditor/Config/MuEditorConfig.h" @@ -157,6 +158,9 @@ void CMuItemEditorUI::Render(bool& showEditor) ImGui::Checkbox(I18N::Editor::FreezeIndexName, &m_bFreezeColumns); ImGui::Separator(); + // The looks of the selected item (read only). + CItemEditorLooks::Render(m_selectedRow); + // Convert search to lowercase for case-insensitive search std::string searchLower = m_szItemSearchBuffer; std::transform(searchLower.begin(), searchLower.end(), searchLower.begin(), ::tolower); diff --git a/src/bin/Data/Items/Models/Group03_Spear.json b/src/bin/Data/Items/Models/Group03_Spear.json index 24d05e895..cd5368d14 100644 --- a/src/bin/Data/Items/Models/Group03_Spear.json +++ b/src/bin/Data/Items/Models/Group03_Spear.json @@ -135,7 +135,8 @@ }, "glow": { "color": "red" - } + }, + "effect": "billOfBalrog" }, { "number": 10, diff --git a/src/bin/Data/Items/Models/Group05_Staff.json b/src/bin/Data/Items/Models/Group05_Staff.json index 98c7fc2d7..7415294d8 100644 --- a/src/bin/Data/Items/Models/Group05_Staff.json +++ b/src/bin/Data/Items/Models/Group05_Staff.json @@ -171,7 +171,8 @@ "glow": { "color": "bronze" }, - "renderStyle": "staffOfKundun" + "renderStyle": "staffOfKundun", + "effect": "staffOfKundun" }, { "number": 12, diff --git a/src/bin/Data/Items/Models/Group07_Helm.json b/src/bin/Data/Items/Models/Group07_Helm.json index a7b2f02bd..b80de398a 100644 --- a/src/bin/Data/Items/Models/Group07_Helm.json +++ b/src/bin/Data/Items/Models/Group07_Helm.json @@ -291,7 +291,8 @@ }, "glow": { "color": "steelBlue" - } + }, + "effect": "divineSet" }, { "number": 21, @@ -340,7 +341,8 @@ }, "glow": { "color": "copper" - } + }, + "effect": "redSpirit" }, { "number": 25, diff --git a/src/bin/Data/Items/Models/Group09_Pants.json b/src/bin/Data/Items/Models/Group09_Pants.json index 23c60acd1..a15617f3c 100644 --- a/src/bin/Data/Items/Models/Group09_Pants.json +++ b/src/bin/Data/Items/Models/Group09_Pants.json @@ -421,7 +421,8 @@ }, "glow": { "color": "copper" - } + }, + "effect": "redSpirit" }, { "number": 25, diff --git a/src/bin/Data/Items/Models/Group10_Gloves.json b/src/bin/Data/Items/Models/Group10_Gloves.json index 7ddf47cb9..86efec582 100644 --- a/src/bin/Data/Items/Models/Group10_Gloves.json +++ b/src/bin/Data/Items/Models/Group10_Gloves.json @@ -329,7 +329,8 @@ }, "glow": { "color": "steelBlue" - } + }, + "effect": "divineSet" }, { "number": 20, diff --git a/src/bin/Data/Items/Models/Group11_Boots.json b/src/bin/Data/Items/Models/Group11_Boots.json index 3a2c243b1..e659fa582 100644 --- a/src/bin/Data/Items/Models/Group11_Boots.json +++ b/src/bin/Data/Items/Models/Group11_Boots.json @@ -229,7 +229,8 @@ }, "glow": { "color": "steelBlue" - } + }, + "effect": "divineSet" }, { "number": 20, diff --git a/src/bin/Data/Items/Models/Group12_Wing.json b/src/bin/Data/Items/Models/Group12_Wing.json index 050768b82..bf5ca26e9 100644 --- a/src/bin/Data/Items/Models/Group12_Wing.json +++ b/src/bin/Data/Items/Models/Group12_Wing.json @@ -77,7 +77,8 @@ "glow": { "level": 0, "excellent": false - } + }, + "effect": "wingsOfSoul" }, { "number": 5, @@ -93,7 +94,8 @@ "glow": { "level": 0, "excellent": false - } + }, + "effect": "wingsOfDragon" }, { "number": 6, @@ -110,7 +112,8 @@ "level": 0, "excellent": false }, - "renderStyle": "wingsOfDarkness" + "renderStyle": "wingsOfDarkness", + "effect": "wingsOfDarkness" }, { "number": 7, @@ -319,7 +322,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "effect": "darkLordScroll" }, { "number": 22, @@ -330,7 +334,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "effect": "darkLordScroll" }, { "number": 23, @@ -341,7 +346,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "effect": "darkLordScroll" }, { "number": 24, @@ -352,7 +358,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "effect": "darkLordScroll" }, { "number": 30, @@ -432,7 +439,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "effect": "darkLordScroll" }, { "number": 36, @@ -448,7 +456,8 @@ "level": 0, "excellent": false }, - "renderStyle": "wingOfStorm" + "renderStyle": "wingOfStorm", + "effect": "wingOfStorm" }, { "number": 37, @@ -464,7 +473,8 @@ "glow": { "level": 0, "excellent": false - } + }, + "effect": "wingOfEternal" }, { "number": 38, @@ -480,7 +490,8 @@ "glow": { "level": 0, "excellent": false - } + }, + "effect": "wingOfIllusion" }, { "number": 39, @@ -498,7 +509,8 @@ "level": 0, "excellent": false }, - "renderStyle": "wingOfRuin" + "renderStyle": "wingOfRuin", + "effect": "wingOfRuin" }, { "number": 40, @@ -565,7 +577,8 @@ "level": 0, "excellent": false }, - "renderStyle": "wingOfDimension" + "renderStyle": "wingOfDimension", + "effect": "wingOfDimension" }, { "number": 44, @@ -640,7 +653,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "effect": "darkLordScroll" }, { "number": 49, @@ -691,6 +705,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeed" }, { @@ -704,6 +721,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeed" }, { @@ -717,6 +737,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeed" }, { @@ -730,6 +753,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeed" }, { @@ -743,6 +769,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeed" }, { @@ -756,6 +785,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeed" }, { @@ -768,6 +800,9 @@ }, "ground": { "scale": 0.6 + }, + "glow": { + "level": 0 } }, { @@ -780,6 +815,9 @@ }, "ground": { "scale": 0.6 + }, + "glow": { + "level": 0 } }, { @@ -792,6 +830,9 @@ }, "ground": { "scale": 0.6 + }, + "glow": { + "level": 0 } }, { @@ -827,6 +868,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -840,6 +884,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -853,6 +900,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -866,6 +916,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -879,6 +932,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -892,6 +948,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -905,6 +964,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -918,6 +980,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -931,6 +996,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -944,6 +1012,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -957,6 +1028,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -970,6 +1044,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -983,6 +1060,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -996,6 +1076,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1009,6 +1092,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1022,6 +1108,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1035,6 +1124,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1048,6 +1140,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1061,6 +1156,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1074,6 +1172,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1087,6 +1188,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1100,6 +1204,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1113,6 +1220,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1126,6 +1236,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1139,6 +1252,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1152,6 +1268,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1165,6 +1284,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1178,6 +1300,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1191,6 +1316,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { @@ -1204,6 +1332,9 @@ "ground": { "scale": 0.6 }, + "glow": { + "level": 0 + }, "renderStyle": "socketSeedSphere" }, { diff --git a/src/bin/Data/Items/Models/Group13_Helper.json b/src/bin/Data/Items/Models/Group13_Helper.json index 8ae07207c..140ac1901 100644 --- a/src/bin/Data/Items/Models/Group13_Helper.json +++ b/src/bin/Data/Items/Models/Group13_Helper.json @@ -66,7 +66,8 @@ "inventory": { "anchor": [0.5, 0.9] }, - "renderStyle": "contractSummon" + "renderStyle": "contractSummon", + "effect": "hiddenMeshByLevel" }, { "number": 8, @@ -102,7 +103,8 @@ "inventory": { "anchor": [0.5, 0.8] }, - "renderStyle": "lifeStone" + "renderStyle": "lifeStone", + "effect": "hideMesh1" }, { "number": 12, @@ -141,7 +143,8 @@ "inventory": { "anchor": [0.6, 1], "scale": 0.003 - } + }, + "effect": "fruits" }, { "number": 16, @@ -167,7 +170,8 @@ "ground": { "rotation": [-45, -5, 18], "scale": 0.48 - } + }, + "effect": "bloodBone" }, { "number": 18, @@ -181,7 +185,8 @@ "ground": { "rotation": [165, -168, 198], "scale": 0.48 - } + }, + "effect": "invisibilityCloak" }, { "number": 21, @@ -302,7 +307,8 @@ "inventory": { "anchor": [0.5, 0.9], "rotation": [-90, -90, 0] - } + }, + "effect": "spirit" }, { "number": 32, @@ -395,7 +401,8 @@ "anchor": [0.5, 0.7], "offset": [0, 0.02], "rotation": [-198, 0, 0] - } + }, + "effect": "harmonyShine" }, { "number": 39, @@ -459,7 +466,8 @@ "rotation": [90, 0, -45], "scale": 0.5 }, - "renderStyle": "sealOfAscension" + "renderStyle": "sealOfAscension", + "effect": "sealShine" }, { "number": 44, @@ -475,7 +483,8 @@ "rotation": [90, 0, -45], "scale": 0.5 }, - "renderStyle": "sealOfWealth" + "renderStyle": "sealOfWealth", + "effect": "sealShine" }, { "number": 45, @@ -491,7 +500,8 @@ "rotation": [90, 0, -45], "scale": 0.5 }, - "renderStyle": "sealOfSustenance" + "renderStyle": "sealOfSustenance", + "effect": "sealShine" }, { "number": 46, @@ -574,7 +584,8 @@ "level": 0, "shineHiddenMesh": 1 }, - "renderStyle": "illusionSorcererCovenant" + "renderStyle": "illusionSorcererCovenant", + "effect": "harmonyShine" }, { "number": 51, @@ -1160,7 +1171,8 @@ "rotation": [0, 0, 90], "scale": 0.5 }, - "renderStyle": "sealOfAscension" + "renderStyle": "sealOfAscension", + "effect": "sealShine" }, { "number": 94, @@ -1172,7 +1184,8 @@ "rotation": [-90, 0, 0], "scale": 0.0021 }, - "renderStyle": "sealOfWealth" + "renderStyle": "sealOfWealth", + "effect": "sealShine" }, { "number": 97, @@ -1402,7 +1415,8 @@ "rotation": [90, 0, -45], "scale": 0.5 }, - "renderStyle": "sealOfWealth" + "renderStyle": "sealOfWealth", + "effect": "sealShine" }, { "number": 121, diff --git a/src/bin/Data/Items/Models/Group14_Potion.json b/src/bin/Data/Items/Models/Group14_Potion.json index 3d462a0f3..a29bd41cc 100644 --- a/src/bin/Data/Items/Models/Group14_Potion.json +++ b/src/bin/Data/Items/Models/Group14_Potion.json @@ -9,7 +9,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "effect": "potion" }, { "number": 1, @@ -18,7 +19,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "effect": "potion" }, { "number": 2, @@ -27,7 +29,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "effect": "potion" }, { "number": 3, @@ -36,7 +39,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "effect": "potion" }, { "number": 4, @@ -45,7 +49,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "effect": "potion" }, { "number": 5, @@ -54,7 +59,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "effect": "potion" }, { "number": 6, @@ -63,7 +69,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "effect": "potion" }, { "number": 7, @@ -72,7 +79,8 @@ "inventory": { "anchor": [0.5, 0.5] }, - "renderStyle": "siegePotion" + "renderStyle": "siegePotion", + "effect": "hiddenMeshByLevel" }, { "number": 8, @@ -156,6 +164,9 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 + }, + "glow": { + "level": 0 } }, { @@ -186,7 +197,8 @@ }, "glow": { "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] - } + }, + "effect": "devilsEye" }, { "number": 18, @@ -201,7 +213,8 @@ }, "glow": { "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] - } + }, + "effect": "devilsKey" }, { "number": 19, @@ -216,7 +229,8 @@ }, "glow": { "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] - } + }, + "effect": "devilsInvitation" }, { "number": 20, @@ -245,7 +259,8 @@ }, "ground": { "rotation": [270, 0, 90] - } + }, + "effect": "rena" }, { "number": 22, @@ -321,7 +336,8 @@ "anchor": [0.5, 0.95], "rotation": [270, 0, 0], "scale": 0.0035 - } + }, + "effect": "meshesPerLevel" }, { "number": 28, @@ -486,7 +502,8 @@ }, "glow": { "level": 0 - } + }, + "effect": "harmonyShine" }, { "number": 43, @@ -624,7 +641,8 @@ "ground": { "rotation": [0, 0, -10], "scale": 0.4 - } + }, + "effect": "gmGift" }, { "number": 53, @@ -750,7 +768,8 @@ "ground": { "rotation": [70, 0, -45], "scale": 1.5 - } + }, + "effect": "firecracker" }, { "number": 64, @@ -769,7 +788,8 @@ "level": 0, "shineHiddenMesh": 0 }, - "renderStyle": "cursedCastleWater" + "renderStyle": "cursedCastleWater", + "effect": "cursedCastleWater" }, { "number": 65, diff --git a/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp b/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp index 135fa321d..a1c1fe03c 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp +++ b/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp @@ -108,18 +108,19 @@ void CheckGlowMeshes(int itemType, const ItemModelDefinition& model) }); } -void CheckRenderStyle(const ItemModelDefinition& model, RenderStyleExists renderStyleExists) +void CheckLookName(const ItemModelDefinition& model, const std::string& name, LookExists exists, + ItemModelProblemType unknown) { - if (model.renderStyle.empty() || renderStyleExists(model.renderStyle)) + if (name.empty() || exists(name)) { return; } - ItemModelProblem problem = MakeProblem(ItemModelProblemType::RenderStyleUnknown, model); - problem.renderStyle = model.renderStyle; + ItemModelProblem problem = MakeProblem(unknown, model); + problem.name = name; AddProblem(std::move(problem)); } -void OpenModel(int itemType, const ItemModelDefinition& model, RenderStyleExists renderStyleExists) +void OpenModel(int itemType, const ItemModelDefinition& model, const LookNames& lookNames) { const std::wstring path = ToLoaderPath(model.file); const size_t nameStart = path.find_last_of(LoaderSeparator) + 1; // 0 when there is no folder @@ -133,7 +134,8 @@ void OpenModel(int itemType, const ItemModelDefinition& model, RenderStyleExists } MarkNoneBlendMeshes(itemType, model); CheckGlowMeshes(itemType, model); - CheckRenderStyle(model, renderStyleExists); + CheckLookName(model, model.renderStyle, lookNames.renderStyle, ItemModelProblemType::RenderStyleUnknown); + CheckLookName(model, model.effect, lookNames.effect, ItemModelProblemType::EffectUnknown); } ItemModelProblemType GetTextureProblemType(const TextureProblem& textureProblem) @@ -187,10 +189,10 @@ template void ForEachModel(TOpen&& open) } } // namespace -void OpenModels(RenderStyleExists renderStyleExists) +void OpenModels(const LookNames& lookNames) { - ForEachModel([renderStyleExists](int itemType, const ItemModelDefinition& model) - { OpenModel(itemType, model, renderStyleExists); }); + ForEachModel([&lookNames](int itemType, const ItemModelDefinition& model) + { OpenModel(itemType, model, lookNames); }); } void OpenTextures() diff --git a/src/source/Data/DataHandler/ItemData/ItemModelLoader.h b/src/source/Data/DataHandler/ItemData/ItemModelLoader.h index b255a65d0..a0dae11a8 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelLoader.h +++ b/src/source/Data/DataHandler/ItemData/ItemModelLoader.h @@ -8,13 +8,20 @@ // data must be loaded first (CItemDataHandler::LoadModels). namespace Data::Items::ModelLoader { -// Whether a render style of this name exists (the render styles are drawing -// code, Render::Items::Styles::Exists). -using RenderStyleExists = bool (*)(std::string_view name); +// Whether a look of this name exists. +using LookExists = bool (*)(std::string_view name); -// Opens the .bmd file of every item model; `renderStyleExists` checks the -// "renderStyle" names. -void OpenModels(RenderStyleExists renderStyleExists); +// The looks that exist; they are drawing code (Render::Items::Styles::Exists, +// Render::Items::Effects::Exists). +struct LookNames +{ + LookExists renderStyle; + LookExists effect; +}; + +// Opens the .bmd file of every item model; `lookNames` checks the names of +// the "renderStyle" and "effect" values. +void OpenModels(const LookNames& lookNames); // Loads the textures of every item model from its texture folders. void OpenTextures(); diff --git a/src/source/Data/DataHandler/ItemData/ItemModelProblem.cpp b/src/source/Data/DataHandler/ItemData/ItemModelProblem.cpp index 3d8521db6..16d394d24 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelProblem.cpp +++ b/src/source/Data/DataHandler/ItemData/ItemModelProblem.cpp @@ -63,7 +63,9 @@ std::string DescribeProblem(const ItemModelProblem& problem) return problem.field + " has mesh " + std::to_string(problem.mesh) + ", but " + problem.modelFile + " has " + std::to_string(problem.meshCount) + " meshes"; case ItemModelProblemType::RenderStyleUnknown: - return "render style " + problem.renderStyle + " does not exist; " + problem.modelFile + " is drawn plainly"; + return "render style " + problem.name + " does not exist; " + problem.modelFile + " is drawn plainly"; + case ItemModelProblemType::EffectUnknown: + return "effect " + problem.name + " does not exist; " + problem.modelFile + " is drawn without it"; } return {}; } diff --git a/src/source/Data/DataHandler/ItemData/ItemModelProblem.h b/src/source/Data/DataHandler/ItemData/ItemModelProblem.h index aad788bb7..48f3c9c7c 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelProblem.h +++ b/src/source/Data/DataHandler/ItemData/ItemModelProblem.h @@ -31,6 +31,9 @@ enum class ItemModelProblemType // "renderStyle" names a style that does not exist; the model is drawn // plainly. RenderStyleUnknown, + // "effect" names an effect that does not exist; the model is drawn + // without it. + EffectUnknown, }; // A problem found while opening an item model or its textures. @@ -50,8 +53,8 @@ struct ItemModelProblem int meshCount = 0; // GlowMeshMissing: the glow value with the mesh, e.g. "glow.meshes". std::string field; - // RenderStyleUnknown: the name of the style. - std::string renderStyle; + // RenderStyleUnknown, EffectUnknown: the name that does not exist. + std::string name; // Only the problems that leave an item without its model or a texture // are errors that the player sees; the others are warnings in the log. diff --git a/src/source/Data/GameData/ItemData/ItemModelDefinition.h b/src/source/Data/GameData/ItemData/ItemModelDefinition.h index dd26a96de..e140f6f27 100644 --- a/src/source/Data/GameData/ItemData/ItemModelDefinition.h +++ b/src/source/Data/GameData/ItemData/ItemModelDefinition.h @@ -127,6 +127,10 @@ struct ItemModelDefinition // The look of the model when it is more than a plain textured model: the // name of a render style (Render/Items/ItemRenderStyles). Empty: plain. std::string renderStyle; + // What the model does besides being drawn (sprites and particles on its + // bones, a pulsing glow mesh, its own drawing, ...): the name of an item + // effect (Render/Items/ItemEffects). Empty: none. + std::string effect; bool Exists() const { diff --git a/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp b/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp index c6d922952..f6b43d6f2 100644 --- a/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp +++ b/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp @@ -24,6 +24,7 @@ constexpr const char* File = "file"; constexpr const char* TextureFolders = "textureFolders"; constexpr const char* NoneBlendMeshes = "noneBlendMeshes"; constexpr const char* RenderStyle = "renderStyle"; +constexpr const char* Effect = "effect"; } // namespace Keys constexpr std::string_view ModelFileExtension = ".bmd"; @@ -86,6 +87,10 @@ OrderedJson WriteModel(const ItemModelDefinition& model) { json[Keys::RenderStyle] = model.renderStyle; } + if (!model.effect.empty()) + { + json[Keys::Effect] = model.effect; + } return json; } @@ -111,7 +116,7 @@ class ItemModelReader bool ReadFile(const OrderedJson& json, std::string& file); void ReadTextureFolders(const OrderedJson& json, std::vector& folders); void ReadNoneBlendMeshes(const OrderedJson& json, std::vector& meshes); - void ReadRenderStyle(const OrderedJson& json, std::string& renderStyle); + void ReadLookName(const OrderedJson& json, const char* key, std::string& name); void WarnAboutUnknownKeys(const OrderedJson& json); const std::string& m_source; @@ -209,20 +214,21 @@ void ItemModelReader::ReadNoneBlendMeshes(const OrderedJson& json, std::vectoris_string() || !Json::IsName(field->get_ref())) { - AddError(Keys::RenderStyle, "must be a name of letters and digits"); + AddError(key, "must be a name of letters and digits"); return; } - renderStyle = field->get(); + name = field->get(); } void ItemModelReader::WarnAboutUnknownKeys(const OrderedJson& json) @@ -235,7 +241,8 @@ void ItemModelReader::WarnAboutUnknownKeys(const OrderedJson& json) DisplayJson::GroundKey, DisplayJson::ClothKey, GlowJson::GlowKey, - Keys::RenderStyle}; + Keys::RenderStyle, + Keys::Effect}; for (const auto& [key, value] : json.items()) { if (!KnownKeys.contains(key)) @@ -267,7 +274,8 @@ bool ItemModelReader::Read(const OrderedJson& json, ItemModelDefinition& model) { AddIssue(severity, field, message); }; DisplayJson::Read(json, model, report); GlowJson::Read(json, model, report); - ReadRenderStyle(json, model.renderStyle); + ReadLookName(json, Keys::RenderStyle, model.renderStyle); + ReadLookName(json, Keys::Effect, model.effect); WarnAboutUnknownKeys(json); return !m_hasErrors; } diff --git a/src/source/Engine/Object/ZzzObject.cpp b/src/source/Engine/Object/ZzzObject.cpp index d20433603..c8b28e326 100644 --- a/src/source/Engine/Object/ZzzObject.cpp +++ b/src/source/Engine/Object/ZzzObject.cpp @@ -46,6 +46,7 @@ #include "GameLogic/Items/ItemCategories.h" #include "Render/Items/ItemDisplay.h" #include "Render/Items/ItemGlow.h" +#include "Render/Items/ItemEffects.h" #include "Render/Items/ItemRenderStyles.h" // DevEditor function declarations @@ -6637,233 +6638,45 @@ void NextGradeObjectRender(CHARACTER* c) } //for } -extern float g_Luminosity; - -void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int ItemLevel, int ExcellentFlags, int ancientDiscriminator, int Select, int RenderType) +// The effects of the event models of level variants, which have no model +// entry yet (the items have theirs in Render/Items/ItemEffects). +static Render::Items::Effects::Result ApplyEventModelEffect(BMD* b, OBJECT* o, int Type, int Level, int ItemLevel) { - int Level = ItemLevel; - if (RenderType & RENDER_WAVE) - { - Level = 0; - } - BMD* b = &Models[Type]; - - if (o->EnableShadow == true) - { - if (gMapManager.WorldActive == 7) - { - EnableAlphaTest(); - } - else - { - DisableAlphaBlend(); - } - bool bRenderShadow = true; - - if (gMapManager.InHellas()) - { - bRenderShadow = false; - } - - if (WD_10HEAVEN == gMapManager.WorldActive || o->m_bySkillCount == 3 || g_Direction.m_CKanturu.IsMayaScene()) - { - bRenderShadow = false; - } - - if (g_isCharacterBuff(o, eBuff_Cloaking)) - { - bRenderShadow = false; - } - - if (bRenderShadow) - { - if (o->m_bRenderShadow) - { - int iHiddenMesh = o->HiddenMesh; - - if (o->Type == MODEL_DREADFEAR) - { - iHiddenMesh = 2; - } - else if (o->Type == MODEL_GAYION) - { - iHiddenMesh = 0; - } - - b->RenderBodyShadow(o->BlendMesh, iHiddenMesh); - } - } - return; - } - - Level = Render::Items::Glow::GetLevel(Type, Level); - - if (g_pOption->GetRenderLevel() < 4) - { - Level = std::min(Level, g_pOption->GetRenderLevel() * 2 + 5); - } - - if (o->Type == MODEL_BILL_OF_BALROG) - { - Vector(0.5f, 0.5f, 1.5f, b->BodyLight); - b->StreamMesh = 0; - b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME); - b->StreamMesh = -1; - } - else if (o->Type == MODEL_POTION + 27) - { - 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) - { - } - else if (Level == 2) - { - b->RenderMesh(1, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - Vector(0.75f, 0.65f, 0.5f, b->BodyLight); - b->RenderMesh(1, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, BITMAP_CHROME); - } - else if (Level == 3) - { - b->RenderMesh(1, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - b->RenderMesh(2, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - Vector(0.75f, 0.65f, 0.5f, b->BodyLight); - b->RenderMesh(1, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, BITMAP_CHROME); - b->RenderMesh(2, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, BITMAP_CHROME); - } - b->StreamMesh = -1; - return; - } - else if (o->Type == MODEL_FIRECRACKER) - { - b->StreamMesh = 0; - o->BlendMeshLight = 1.f; - Vector(1.f, 1.f, 1.f, b->BodyLight); - b->RenderMesh(0, RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - Vector(1.f, 0.f, 0.f, b->BodyLight); - b->LightEnable = true; - b->RenderMesh(1, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - b->RenderMesh(1, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - b->StreamMesh = -1; - return; - } - else if (o->Type == MODEL_GM_GIFT) - { - Vector(1.f, 1.f, 1.f, b->BodyLight); - b->RenderBody(RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - b->LightEnable = true; - Vector(0.1f, 0.6f, 0.4f, b->BodyLight); - o->Alpha = 0.5f; - b->RenderMesh(0, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - return; - } - else if (o->Type == MODEL_EVENT + 14 && Level == 9) + using Render::Items::Effects::Result; + if (o->Type == MODEL_EVENT + 14 && Level == 9) { Vector(0.3f, 0.8f, 1.f, b->BodyLight); b->RenderBody(RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); Vector(1.f, 0.8f, 0.3f, b->BodyLight); b->RenderBody(RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - return; - } - else if ((o->Type >= MODEL_SCROLL_OF_FIREBURST && o->Type <= MODEL_SCROLL_OF_ELECTRIC_SPARK) - || o->Type == MODEL_SCROLL_OF_FIRE_SCREAM - || (o->Type == MODEL_SCROLL_OF_CHAOTIC_DISEIER) - ) - { - b->BeginRender(o->Alpha); - o->BlendMeshLight = 1.f; - b->RenderMesh(0, RENDER_TEXTURE, Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); - o->BlendMeshLight = sinf(WorldTime * 0.001f) * 0.5f + 0.5f; - b->RenderMesh(1, RENDER_BRIGHT | RENDER_TEXTURE, Alpha, 1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); - b->RenderMesh(2, RENDER_BRIGHT | RENDER_TEXTURE, Alpha, 2, 1 - o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); - b->EndRender(); - return; + return Result::Drawn; } - else if (o->Type == MODEL_SPIRIT) - { - b->RenderBody(RENDER_TEXTURE, Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); - switch (Level) - { - case 0: - b->RenderMesh(0, RENDER_BRIGHT | RENDER_TEXTURE, Alpha, 0, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); - break; - - case 1: - Vector(0.3f, 0.8f, 1.f, b->BodyLight); - b->RenderMesh(0, RENDER_BRIGHT | RENDER_TEXTURE, Alpha, 0, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); - break; - } - return; - } - else if (o->Type >= MODEL_POTION && o->Type <= MODEL_LARGE_MANA_POTION) - { - if (Level > 0) - Level = 7; - } - else if (GameLogic::Items::IsSocketSeedOrSphereModel(o->Type)) - { - Level = 0; - } - else if (o->Type == MODEL_FRUITS) - { - switch (Level) - { - case 0: Vector(0.0f, 0.5f, 1.0f, b->BodyLight); break; - case 1: Vector(1.0f, 0.2f, 0.0f, b->BodyLight); break; - case 2: Vector(1.0f, 0.8f, 0.0f, b->BodyLight); break; - case 3: Vector(0.6f, 0.8f, 0.4f, b->BodyLight); break; - } - b->RenderBody(RENDER_METAL, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME + 1); - b->RenderBody(RENDER_BRIGHT | RENDER_CHROME, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME + 1); - return; - } - else if (o->Type == MODEL_EVENT + 11) + if (o->Type == MODEL_EVENT + 11) { Vector(0.9f, 0.9f, 0.9f, b->BodyLight); b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); b->RenderBody(RENDER_CHROME | RENDER_BRIGHT, 0.5f, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, -1, BITMAP_CHROME + 1); - return; + return Result::Drawn; } - else if (Type == MODEL_EVENT + 5 && ItemLevel == 14) + if (Type == MODEL_EVENT + 5 && ItemLevel == 14) { Vector(0.2f, 0.3f, 0.5f, b->BodyLight); b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); Vector(0.1f, 0.3f, 1.f, b->BodyLight); b->RenderBody(RENDER_CHROME | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); b->RenderBody(RENDER_METAL | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - return; + return Result::Drawn; } - else if (Type == MODEL_EVENT + 5 && ItemLevel == 15) + if (Type == MODEL_EVENT + 5 && ItemLevel == 15) { Vector(0.5f, 0.3f, 0.2f, b->BodyLight); b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); Vector(1.f, 0.3f, 0.1f, b->BodyLight); b->RenderBody(RENDER_CHROME | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); b->RenderBody(RENDER_METAL | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); - return; - } - else if (o->Type == MODEL_BLOOD_BONE) - { - o->BlendMeshTexCoordU = sinf(gMapManager.WorldActive * 0.0001f); - o->BlendMeshTexCoordV = -WorldTime * 0.0005f; - Vector(.9f, .9f, .9f, b->BodyLight); - 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; - b->RenderBody(RENDER_TEXTURE | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME); - Models[o->Type].StreamMesh = -1; - return; + return Result::Drawn; } - else if (o->Type == MODEL_INVISIBILITY_CLOAK) - { - Vector(0.8f, 0.8f, 0.8f, b->BodyLight); - float sine = float(sinf(WorldTime * 0.002f) * 0.3f) + 0.7f; - - b->RenderBody(RENDER_TEXTURE | RENDER_BRIGHT, 1.0f, 0, sine, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); - return; - } - else if (o->Type == MODEL_EVENT + 12) + if (o->Type == MODEL_EVENT + 12) { float Luminosity = (float)sinf((WorldTime) * 0.002f) * 0.35f + 0.65f; vec3_t p, Position, EffLight; @@ -6876,15 +6689,16 @@ void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int VectorAdd(Position, o->Position, Position); CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); + return Result::Applied; } - else if (o->Type == MODEL_EVENT + 6 && Level == 13) + if (o->Type == MODEL_EVENT + 6 && Level == 13) { Vector(0.4f, 0.6f, 1.0f, b->BodyLight); b->RenderBody(RENDER_COLOR, 1.0f, 0, 1.0f, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); b->RenderBody(RENDER_CHROME | RENDER_BRIGHT, 1.0f, 0, 1.0f, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh); - return; + return Result::Drawn; } - else if (o->Type == MODEL_EVENT + 13) + if (o->Type == MODEL_EVENT + 13) { float Luminosity = (float)sinf((WorldTime) * 0.002f) * 0.35f + 0.65f; vec3_t p, Position, EffLight; @@ -6900,359 +6714,93 @@ void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int VectorAdd(Position, o->Position, Position); CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); + return Result::Applied; } - else if (o->Type == MODEL_DEVILS_EYE) + if (Type == MODEL_EVENT + 18) { - float sine = (float)sinf(WorldTime * 0.002f) * 10.f + 15.65f; - o->BlendMesh = 1; - o->BlendMeshLight = sine; - o->BlendMeshTexCoordV = (int)WorldTime % 2000 * 0.0005f; - o->Alpha = 2.0f; - - float Luminosity = sine; - Vector(Luminosity / 5.0f, Luminosity / 5.0f, Luminosity / 5.0f, o->Light); + return Result::Applied; } - else if (o->Type == MODEL_DEVILS_KEY) - { - float Luminosity = (float)sinf((WorldTime) * 0.002f) * 0.35f + 0.65f; - vec3_t p, Position, EffLight; - Vector(0.f, 0.f, 0.f, p); - - float Scale = Luminosity * 0.8f; - Vector(Luminosity * 2, Luminosity * 0.32f, Luminosity * 0.32f, EffLight); + return Result::None; +} - b->TransformPosition(BoneTransform[1], p, Position); - VectorAdd(Position, o->Position, Position); - CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); +extern float g_Luminosity; - b->TransformPosition(BoneTransform[2], p, Position); - VectorAdd(Position, o->Position, Position); - CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); - } - else if (o->Type == MODEL_DEVILS_INVITATION) +void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int ItemLevel, int ExcellentFlags, + int ancientDiscriminator, int Select, int RenderType) +{ + int Level = ItemLevel; + if (RenderType & RENDER_WAVE) { - float Luminosity = (float)sinf((WorldTime) * 0.002f) * 0.35f + 0.65f; - vec3_t p, Position, EffLight; - Vector(0.f, 0.f, 0.f, p); - - float Scale = Luminosity * 0.8f; - Vector(Luminosity * 2, Luminosity * 0.32f, Luminosity * 0.32f, EffLight); - - b->TransformPosition(BoneTransform[9], p, Position); - VectorAdd(Position, o->Position, Position); - CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); - - b->TransformPosition(BoneTransform[10], p, Position); - VectorAdd(Position, o->Position, Position); - CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); + Level = 0; } - else if (o->Type == MODEL_RENA) - { - float Luminosity = (float)sinf((WorldTime) * 0.002f) * 0.25f + 0.75f; - vec3_t EffLight; + BMD* b = &Models[Type]; - Vector(Luminosity * 1.f, Luminosity * 0.5f, Luminosity * 0.f, EffLight); - CreateSprite(BITMAP_SPARK + 1, o->Position, 2.5f, EffLight, o); - } - else if (o->Type == MODEL_WINGS_OF_DRAGON) - { - o->BlendMeshLight = (float)(sinf(WorldTime * 0.001f) + 1.f) / 4.f; - } - else if (o->Type == MODEL_WINGS_OF_SOUL) - { - o->BlendMeshLight = (float)sinf(WorldTime * 0.001f) + 1.1f; - } - else if (Type == MODEL_RED_SPIRIT_PANTS || Type == MODEL_RED_SPIRIT_HELM) - { - o->BlendMeshLight = sinf(WorldTime * 0.001f) * 0.4f + 0.6f; - } - else if (o->Type == MODEL_STAFF_OF_KUNDUN) - { - o->BlendMeshLight = sinf(WorldTime * 0.004f) * 0.3f + 0.7f; - } - else if (Type == MODEL_DIVINE_HELM || Type == MODEL_DIVINE_GLOVES || Type == MODEL_DIVINE_BOOTS) - { - o->BlendMeshLight = 1.f; - } - else if (Type == MODEL_SIEGE_POTION) + if (o->EnableShadow == true) { - switch (Level) + if (gMapManager.WorldActive == 7) { - case 0: o->HiddenMesh = 1; break; - case 1: o->HiddenMesh = 0; break; + EnableAlphaTest(); } - } - else if (Type == MODEL_CONTRACT_SUMMON) - { - switch (Level) + else { - case 0: o->HiddenMesh = 1; break; - case 1: o->HiddenMesh = 0; break; + DisableAlphaBlend(); } - } - else if (Type == MODEL_LIFE_STONE_ITEM) - { - o->HiddenMesh = 1; - } - else if (Type == MODEL_EVENT + 18) - { - o->BlendMesh = 1; - } - else if (o->Type == MODEL_WINGS_OF_DARKNESS) - { - vec3_t posCenter, p, Position, Light; - float Scale = sinf(WorldTime * 0.004f) * 0.3f + 0.3f; - - Scale = (Scale * 10.f) + 20.f; - - Vector(0.6f, 0.3f, 0.8f, Light); - - Vector(0.f, 0.f, 0.f, p); + bool bRenderShadow = true; - for (int i = 0; i < 5; ++i) + if (gMapManager.InHellas()) { - b->TransformPosition(BoneTransform[22 - i], p, posCenter, true); - b->TransformPosition(BoneTransform[30 - i], p, Position, true); - if (rand_fps_check(1)) - { - CreateJoint(BITMAP_JOINT_THUNDER, Position, posCenter, o->Angle, 14, o, Scale); - CreateJoint(BITMAP_JOINT_SPIRIT, posCenter, Position, o->Angle, 4, o, Scale + 5); - } - - CreateSprite(BITMAP_FLARE_BLUE, posCenter, Scale / 28.f, Light, o); + bRenderShadow = false; } - for (int i = 0; i < 5; ++i) + if (WD_10HEAVEN == gMapManager.WorldActive || o->m_bySkillCount == 3 || g_Direction.m_CKanturu.IsMayaScene()) { - b->TransformPosition(BoneTransform[7 - i], p, posCenter, true); - b->TransformPosition(BoneTransform[11 + i], p, Position, true); - if (rand_fps_check(1)) - { - CreateJoint(BITMAP_JOINT_THUNDER, Position, posCenter, o->Angle, 14, o, Scale); - CreateJoint(BITMAP_JOINT_SPIRIT, posCenter, Position, o->Angle, 4, o, Scale + 5); - } - - CreateSprite(BITMAP_FLARE_BLUE, posCenter, Scale / 28.f, Light, o); + bRenderShadow = false; } - } - else if (Type == MODEL_WING_OF_STORM) - { - vec3_t vRelativePos, vPos, vLight; - Vector(0.f, 0.f, 0.f, vRelativePos); - Vector(0.f, 0.f, 0.f, vPos); - Vector(0.f, 0.f, 0.f, vLight); - - float fLuminosity = absf(sinf(WorldTime * 0.0004f)) * 0.4f; - Vector(0.5f + fLuminosity, 0.5f + fLuminosity, 0.5f + fLuminosity, vLight); - int iBone[] = { 9, 20, 19, 10, 18, - 28, 27, 36, 35, 38, - 37, 53, 48, 62, 70, - 72, 71, 78, 79, 80, - 87, 90, 91, 106, 102 }; - float fScale = 0.f; - for (int i = 0; i < 25; ++i) + if (g_isCharacterBuff(o, eBuff_Cloaking)) { - b->TransformPosition(BoneTransform[iBone[i]], vRelativePos, vPos, true); - fScale = 0.5f;// (rand()%10) * 0.05f + 0.3f; - CreateSprite(BITMAP_CLUD64, vPos, fScale, vLight, o, WorldTime * 0.01f, 1); + bRenderShadow = false; } - int iBoneThunder[] = { 11, 21, 29, 63, 81, 89 }; - if (rand_fps_check(2)) + if (bRenderShadow) { - for (int i = 0; i < 6; ++i) + if (o->m_bRenderShadow) { - b->TransformPosition(BoneTransform[iBoneThunder[i]], vRelativePos, vPos, true); - if (rand_fps_check(20)) + int iHiddenMesh = o->HiddenMesh; + + if (o->Type == MODEL_DREADFEAR) { - Vector(0.6f, 0.6f, 0.9f, vLight); - CreateEffect(MODEL_FENRIR_THUNDER, vPos, o->Angle, vLight, 1, o); + iHiddenMesh = 2; + } + else if (o->Type == MODEL_GAYION) + { + iHiddenMesh = 0; } - } - } - - int iBoneLight[] = { 64, 61, 69, 77, 86, - 98, 97, 99, 104, 103, - 105, 12, 8, 17, 26, - 34, 52, 44, 51, 50, - 49, 45 }; - - fScale = absf(sinf(WorldTime * 0.003f)) * 0.2f; - for (int i = 0; i < 22; ++i) - { - b->TransformPosition(BoneTransform[iBoneLight[i]], vRelativePos, vPos, true); - if (iBoneLight[i] == 12 || iBoneLight[i] == 64 || iBoneLight[i] == 98 || iBoneLight[i] == 52) - { - Vector(0.9f, 0.0f, 0.0f, vLight); - CreateSprite(BITMAP_LIGHT, vPos, fScale + 1.4f, vLight, o); - } - else - { - Vector(0.8f, 0.5f, 0.2f, vLight); - CreateSprite(BITMAP_LIGHT, vPos, fScale + 0.3f, vLight, o); + b->RenderBodyShadow(o->BlendMesh, iHiddenMesh); } } + return; } - else if (Type == MODEL_WING_OF_ETERNAL) - { - vec3_t p, Position, Light; - Vector(0.f, 0.f, 0.f, p); - float Scale = absf(sinf(WorldTime * 0.003f)) * 0.2f; - float Luminosity = absf(sinf(WorldTime * 0.003f)) * 0.3f; - Vector(0.5f + Luminosity, 0.5f + Luminosity, 0.6f + Luminosity, Light); - //int iRedFlarePos[] = { 25, 32, 53, 15, 9, 35 }; - int iRedFlarePos[] = { 24, 31, 15, 8, 53, 35 }; - for (int i = 0; i < 6; ++i) - { - b->TransformPosition(BoneTransform[iRedFlarePos[i]], p, Position, true); - CreateSprite(BITMAP_LIGHT, Position, Scale + 1.3f, Light, o); - } - - Vector(0.1f, 0.1f, 0.9f, Light); - //int iGreenFlarePos[] = { 23, 22, 24, 34, 5, 31, 14, 12, 27, 8, 6, 7, 16, 13, 56, 37, 58, 40, 39, 38 }; - int iGreenFlarePos[] = { 22, 23, 25, 29, 30, 28, 32, 13, 16, 14, 12, 9, 7, 6, 57, 58, 40, 39 }; - - for (int i = 0; i < 18; ++i) - { - b->TransformPosition(BoneTransform[iGreenFlarePos[i]], p, Position, true); - CreateSprite(BITMAP_LIGHT, Position, Scale + 1.5f, Light, o); - } - int iGreenFlarePos2[] = { 56, 38, 51, 45 }; + Level = Render::Items::Glow::GetLevel(Type, Level); - for (int i = 0; i < 4; ++i) - { - b->TransformPosition(BoneTransform[iGreenFlarePos2[i]], p, Position, true); - CreateSprite(BITMAP_LIGHT, Position, Scale + 0.5f, Light, o); - } - } - else if (Type == MODEL_WING_OF_ILLUSION) + if (g_pOption->GetRenderLevel() < 4) { - vec3_t p, Position, Light; - Vector(0.f, 0.f, 0.f, p); - float Scale = absf(sinf(WorldTime * 0.002f)) * 0.2f; - float Luminosity = absf(sinf(WorldTime * 0.002f)) * 0.4f; - - Vector(0.5f + Luminosity, 0.0f + Luminosity, 0.0f + Luminosity, Light); - int iRedFlarePos[] = { 5, 6, 7, 8, 18, 19, 23, 24, 25, 27, 37, 38 }; - for (int i = 0; i < 12; ++i) - { - b->TransformPosition(BoneTransform[iRedFlarePos[i]], p, Position, true); - CreateSprite(BITMAP_FLARE, Position, Scale + 0.6f, Light, o); - } - - Vector(0.0f + Luminosity, 0.5f + Luminosity, 0.0f + Luminosity, Light); - int iGreenFlarePos[] = { 4, 9, 13, 14, 26, 32, 31, 33 }; - - for (int i = 0; i < 8; ++i) - { - b->TransformPosition(BoneTransform[iGreenFlarePos[i]], p, Position, true); - CreateSprite(BITMAP_LIGHT, Position, 1.3f, Light, o); - } - - Vector(1.0f, 1.0f, 1.0f, Light); - float fLumi = (sinf(WorldTime * 0.004f) + 1.0f) * 0.05f; - Vector(0.8f + fLumi, 0.8f + fLumi, 0.3f + fLumi, Light); - CreateSprite(BITMAP_LIGHT, Position, 0.4f, Light, o, 0.5f); - if (rand_fps_check(2)) - { - b->TransformPosition(BoneTransform[13], p, Position, true); - CreateParticle(BITMAP_SHINY, Position, o->Angle, Light, 5, 0.5f); - b->TransformPosition(BoneTransform[31], p, Position, true); - CreateParticle(BITMAP_SHINY, Position, o->Angle, Light, 5, 0.5f); - } + Level = std::min(Level, g_pOption->GetRenderLevel() * 2 + 5); } - else if (Type == MODEL_WING_OF_RUIN) - { - vec3_t p, Position, Light; - Vector(0.f, 0.f, 0.f, p); - float Scale = absf(sinf(WorldTime * 0.003f)) * 0.2f; - float Luminosity = absf(sinf(WorldTime * 0.003f)) * 0.3f; - - Vector(0.7f + Luminosity, 0.5f + Luminosity, 0.8f + Luminosity, Light); - int iRedFlarePos[] = { 6, 15, 24, 56, 47, 38 }; - for (int i = 0; i < 6; ++i) - { - b->TransformPosition(BoneTransform[iRedFlarePos[i]], p, Position, true); - CreateSprite(BITMAP_LIGHT, Position, Scale + 1.5f, Light, o); - } - - Vector(0.6f, 0.4f, 0.7f, Light); - int iSparkPos[] = { 7, 16, 25, 57, 48, 39, - 11, 22, 31, 63, 54, 40, - 10, 21, 30, 62, 53, 41, - 9, 20, 29, 61, 52, 42, - 8, 19, 28, 60, 51, 43, - 18, 27, 59, 50, - 17, 26, 58, 49 }; - int iNumParticle = 1; - - for (int i = 0; i < 6; ++i) - { - b->TransformPosition(BoneTransform[iSparkPos[i]], p, Position, true); - for (int j = 0; j < iNumParticle; ++j) - if (rand_fps_check(1)) - CreateParticle(BITMAP_CHROME_ENERGY2, Position, o->Angle, Light, 0, 0.1f); - } - - for (int i = 6; i < 18; ++i) - { - b->TransformPosition(BoneTransform[iSparkPos[i]], p, Position, true); - for (int j = 0; j < iNumParticle; ++j) - if (rand_fps_check(1)) - CreateParticle(BITMAP_CHROME_ENERGY2, Position, o->Angle, Light, 0, 0.3f); - } - - for (int i = 18; i < 30; ++i) - { - b->TransformPosition(BoneTransform[iSparkPos[i]], p, Position, true); - for (int j = 0; j < iNumParticle; ++j) - if (rand_fps_check(1)) - CreateParticle(BITMAP_CHROME_ENERGY2, Position, o->Angle, Light, 0, 0.5f); - } - for (int i = 30; i < 38; ++i) - { - b->TransformPosition(BoneTransform[iSparkPos[i]], p, Position, true); - for (int j = 0; j < iNumParticle; ++j) - if (rand_fps_check(1)) - CreateParticle(BITMAP_CHROME_ENERGY2, Position, o->Angle, Light, 0, 0.7f); - } + // Items have the effect of their model entry; the event models of level + // variants have theirs in ApplyEventModelEffect. + 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); } - else if (Type == MODEL_WING_OF_DIMENSION) + if (effect == Render::Items::Effects::Result::Drawn) { - vec3_t p, Position, Light; - Vector(0.f, 0.f, 0.f, p); - float Scale = absf(sinf(WorldTime * 0.002f)) * 0.2f; - float Luminosity = absf(sinf(WorldTime * 0.002f)) * 0.4f; - - Vector((1.0f + Luminosity) / 2.f, (0.7f + Luminosity) / 2.f, (0.2f + Luminosity) / 2.f, Light); - int iFlarePos0[] = { 7, 30, 31, 43, 8, 20 }; - - int icnt; - for (icnt = 0; icnt < 2; ++icnt) - { - b->TransformPosition(BoneTransform[iFlarePos0[icnt]], p, Position, true); - CreateSprite(BITMAP_FLARE, Position, Scale + 2.0f, Light, o); - } - Vector((1.0f + Luminosity) / 4.f, (0.7f + Luminosity) / 4.f, (0.2f + Luminosity) / 4.f, Light); - for (; icnt < 6; ++icnt) - { - b->TransformPosition(BoneTransform[iFlarePos0[icnt]], p, Position, true); - CreateSprite(BITMAP_FLARE, Position, Scale + 0.5f, Light, o); - } - - Vector((0.5f + Luminosity) / 2.f, (0.1f + Luminosity) / 2.f, (0.4f + Luminosity) / 2.f, Light); - int iGreenFlarePos[] = { 29, 38, 42, 19, 15, 6 }; - - for (int i = 0; i < 6; ++i) - { - b->TransformPosition(BoneTransform[iGreenFlarePos[i]], p, Position, true); - CreateSprite(BITMAP_FLARE, Position, Scale + 2.0f, Light, o); - } + return; } if (!o->EnableShadow) @@ -7302,56 +6850,14 @@ void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int Vector(Luminosity * 0.3f, Luminosity * 0.5f, Luminosity * 1.f, b->BodyLight); RenderPartObjectBody(b, o, Type, Alpha, RenderType); } - else if (Level < 3 || o->Type == MODEL_ZEN) + else if (Level < 3) { - if (o->Type == MODEL_CURSED_CASTLE_WATER) - { - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - RenderPartObjectBodyColor2(b, o, Type, 0.5f, RENDER_TEXTURE | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 0.5f); - RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); - } - else if (o->Type == MODEL_ILLUSION_SORCERER_COVENANT) + // Some items shine below +3 (the effect of their model entry). + if (!Render::Items::Effects::RenderBelowPlus3(b, o, Type, Alpha, RenderType, Light)) { VectorCopy(Light, b->BodyLight); RenderPartObjectBody(b, o, Type, Alpha, RenderType); - RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); - RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); } - else if (o->Type == MODEL_JEWEL_OF_HARMONY || o->Type == MODEL_MOONSTONE_PENDANT) - { - VectorCopy(Light, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - - RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); - RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); - } - else - if (o->Type == MODEL_SEAL_OF_ASCENSION || o->Type == MODEL_MASTER_SEAL_OF_ASCENSION) - { - Vector(Light[0] * 0.9f, Light[1] * 0.9f, Light[2] * 0.9f, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); - RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); - } - else if (o->Type == MODEL_SEAL_OF_WEALTH || o->Type == MODEL_MASTER_SEAL_OF_WEALTH || o->Type == MODEL_HELPER + 116) - { - Vector(Light[0] * 0.9f, Light[1] * 0.9f, Light[2] * 0.9f, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); - RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); - } - else if (o->Type == MODEL_SEAL_OF_SUSTENANCE) - { - Vector(Light[0] * 0.9f, Light[1] * 0.9f, Light[2] * 0.9f, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); - RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); - } - else - { - VectorCopy(Light, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - } } else if (Level < 5) { diff --git a/src/source/Engine/Object/ZzzOpenData.cpp b/src/source/Engine/Object/ZzzOpenData.cpp index f8afb0b6d..0d835413c 100644 --- a/src/source/Engine/Object/ZzzOpenData.cpp +++ b/src/source/Engine/Object/ZzzOpenData.cpp @@ -33,6 +33,7 @@ #include "Data/DataHandler/ItemData/ItemDataHandler.h" #include "Data/DataHandler/ItemData/ItemModelLoader.h" +#include "Render/Items/ItemEffects.h" #include "Render/Items/ItemRenderStyles.h" #include "Core/Platform/ErrorDialog.h" #include "Core/Text/Utf8.h" @@ -462,7 +463,7 @@ void OpenItems() // Without model data the game stops; the item models are not opened. if (OpenItemModelData()) { - Data::Items::ModelLoader::OpenModels(Render::Items::Styles::Exists); + Data::Items::ModelLoader::OpenModels({Render::Items::Styles::Exists, Render::Items::Effects::Exists}); PrepareGrandSoulPantsClothMesh(); } OpenItemEffectModels(); diff --git a/src/source/Render/Items/ItemEffects.cpp b/src/source/Render/Items/ItemEffects.cpp new file mode 100644 index 000000000..cbe43c742 --- /dev/null +++ b/src/source/Render/Items/ItemEffects.cpp @@ -0,0 +1,661 @@ +#include "stdafx.h" + +#include "ItemEffects.h" +#include "ItemModelLookup.h" + +#include "Core/Globals/_enum.h" +#include "Engine/Object/w_ObjectInfo.h" +#include "Engine/Object/ZzzCharacter.h" +#include "Engine/Object/ZzzObject.h" +#include "Render/Effects/ZzzEffect.h" +#include "Render/Models/ZzzBMD.h" + +#include +#include + +namespace Render::Items::Effects +{ +namespace +{ +// An effect runs before the model is drawn, and may change the level the +// model glows like. Drawn: it drew the model itself, nothing more is drawn. +using EffectFunction = Result (*)(BMD* b, OBJECT* o, int Type, float Alpha, int& Level); +// Draws the model below +3 instead of the plain drawing. +using ShineFunction = void (*)(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType, float* Light); + +struct ItemEffect +{ + const char* name; + EffectFunction apply = nullptr; + ShineFunction renderBelowPlus3 = nullptr; +}; + +// ------------------------------------------------ the effects + +Result ApplyBillOfBalrog(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + Vector(0.5f, 0.5f, 1.5f, b->BodyLight); + b->StreamMesh = 0; + b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME); + b->StreamMesh = -1; + return Result::Applied; +} + +Result ApplyMeshesPerLevel(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + 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) + { + } + else if (Level == 2) + { + b->RenderMesh(1, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); + Vector(0.75f, 0.65f, 0.5f, b->BodyLight); + b->RenderMesh(1, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, BITMAP_CHROME); + } + else if (Level == 3) + { + b->RenderMesh(1, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); + b->RenderMesh(2, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); + Vector(0.75f, 0.65f, 0.5f, b->BodyLight); + b->RenderMesh(1, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, BITMAP_CHROME); + b->RenderMesh(2, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, BITMAP_CHROME); + } + b->StreamMesh = -1; + return Result::Drawn; +} + +Result ApplyFirecracker(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + b->StreamMesh = 0; + o->BlendMeshLight = 1.f; + Vector(1.f, 1.f, 1.f, b->BodyLight); + b->RenderMesh(0, RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV); + Vector(1.f, 0.f, 0.f, b->BodyLight); + b->LightEnable = true; + b->RenderMesh(1, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); + b->RenderMesh(1, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV); + b->StreamMesh = -1; + return Result::Drawn; +} + +Result ApplyGmGift(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + Vector(1.f, 1.f, 1.f, b->BodyLight); + b->RenderBody(RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); + b->LightEnable = true; + Vector(0.1f, 0.6f, 0.4f, b->BodyLight); + o->Alpha = 0.5f; + b->RenderMesh(0, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV); + return Result::Drawn; +} + +Result ApplyDarkLordScroll(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + b->BeginRender(o->Alpha); + o->BlendMeshLight = 1.f; + b->RenderMesh(0, RENDER_TEXTURE, Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, + o->HiddenMesh); + o->BlendMeshLight = sinf(WorldTime * 0.001f) * 0.5f + 0.5f; + b->RenderMesh(1, RENDER_BRIGHT | RENDER_TEXTURE, Alpha, 1, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh); + b->RenderMesh(2, RENDER_BRIGHT | RENDER_TEXTURE, Alpha, 2, 1 - o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh); + b->EndRender(); + return Result::Drawn; +} + +Result ApplySpirit(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + b->RenderBody(RENDER_TEXTURE, Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, + o->HiddenMesh); + switch (Level) + { + case 0: + b->RenderMesh(0, RENDER_BRIGHT | RENDER_TEXTURE, Alpha, 0, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh); + break; + + case 1: + Vector(0.3f, 0.8f, 1.f, b->BodyLight); + b->RenderMesh(0, RENDER_BRIGHT | RENDER_TEXTURE, Alpha, 0, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh); + break; + } + return Result::Drawn; +} + +Result ApplyPotion(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + if (Level > 0) + Level = 7; + return Result::Applied; +} + +Result ApplyFruits(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + switch (Level) + { + case 0: + Vector(0.0f, 0.5f, 1.0f, b->BodyLight); + break; + case 1: + Vector(1.0f, 0.2f, 0.0f, b->BodyLight); + break; + case 2: + Vector(1.0f, 0.8f, 0.0f, b->BodyLight); + break; + case 3: + Vector(0.6f, 0.8f, 0.4f, b->BodyLight); + break; + } + b->RenderBody(RENDER_METAL, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, + o->HiddenMesh, BITMAP_CHROME + 1); + b->RenderBody(RENDER_BRIGHT | RENDER_CHROME, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME + 1); + return Result::Drawn; +} + +Result ApplyBloodBone(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + o->BlendMeshTexCoordU = sinf(gMapManager.WorldActive * 0.0001f); + o->BlendMeshTexCoordV = -WorldTime * 0.0005f; + Vector(.9f, .9f, .9f, b->BodyLight); + 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; + b->RenderBody(RENDER_TEXTURE | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME); + Models[o->Type].StreamMesh = -1; + return Result::Drawn; +} + +Result ApplyInvisibilityCloak(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + Vector(0.8f, 0.8f, 0.8f, b->BodyLight); + float sine = float(sinf(WorldTime * 0.002f) * 0.3f) + 0.7f; + + b->RenderBody(RENDER_TEXTURE | RENDER_BRIGHT, 1.0f, 0, sine, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, + o->HiddenMesh); + return Result::Drawn; +} + +Result ApplyDevilsEye(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + float sine = (float)sinf(WorldTime * 0.002f) * 10.f + 15.65f; + + o->BlendMesh = 1; + o->BlendMeshLight = sine; + o->BlendMeshTexCoordV = (int)WorldTime % 2000 * 0.0005f; + o->Alpha = 2.0f; + + float Luminosity = sine; + Vector(Luminosity / 5.0f, Luminosity / 5.0f, Luminosity / 5.0f, o->Light); + return Result::Applied; +} + +Result ApplyDevilsKey(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + float Luminosity = (float)sinf((WorldTime) * 0.002f) * 0.35f + 0.65f; + vec3_t p, Position, EffLight; + Vector(0.f, 0.f, 0.f, p); + + float Scale = Luminosity * 0.8f; + Vector(Luminosity * 2, Luminosity * 0.32f, Luminosity * 0.32f, EffLight); + + b->TransformPosition(BoneTransform[1], p, Position); + VectorAdd(Position, o->Position, Position); + CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); + + b->TransformPosition(BoneTransform[2], p, Position); + VectorAdd(Position, o->Position, Position); + CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); + return Result::Applied; +} + +Result ApplyDevilsInvitation(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + float Luminosity = (float)sinf((WorldTime) * 0.002f) * 0.35f + 0.65f; + vec3_t p, Position, EffLight; + Vector(0.f, 0.f, 0.f, p); + + float Scale = Luminosity * 0.8f; + Vector(Luminosity * 2, Luminosity * 0.32f, Luminosity * 0.32f, EffLight); + + b->TransformPosition(BoneTransform[9], p, Position); + VectorAdd(Position, o->Position, Position); + CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); + + b->TransformPosition(BoneTransform[10], p, Position); + VectorAdd(Position, o->Position, Position); + CreateSprite(BITMAP_SPARK + 1, Position, Scale, EffLight, o); + return Result::Applied; +} + +Result ApplyRena(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + float Luminosity = (float)sinf((WorldTime) * 0.002f) * 0.25f + 0.75f; + vec3_t EffLight; + + Vector(Luminosity * 1.f, Luminosity * 0.5f, Luminosity * 0.f, EffLight); + CreateSprite(BITMAP_SPARK + 1, o->Position, 2.5f, EffLight, o); + return Result::Applied; +} + +Result ApplyWingsOfDragon(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + o->BlendMeshLight = (float)(sinf(WorldTime * 0.001f) + 1.f) / 4.f; + return Result::Applied; +} + +Result ApplyWingsOfSoul(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + o->BlendMeshLight = (float)sinf(WorldTime * 0.001f) + 1.1f; + return Result::Applied; +} + +Result ApplyRedSpirit(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + o->BlendMeshLight = sinf(WorldTime * 0.001f) * 0.4f + 0.6f; + return Result::Applied; +} + +Result ApplyStaffOfKundun(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + o->BlendMeshLight = sinf(WorldTime * 0.004f) * 0.3f + 0.7f; + return Result::Applied; +} + +Result ApplyDivineSet(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + o->BlendMeshLight = 1.f; + return Result::Applied; +} + +Result ApplyHiddenMeshByLevel(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + switch (Level) + { + case 0: + o->HiddenMesh = 1; + break; + case 1: + o->HiddenMesh = 0; + break; + } + return Result::Applied; +} + +Result ApplyHideMesh1(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + o->HiddenMesh = 1; + return Result::Applied; +} + +Result ApplyWingsOfDarkness(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + vec3_t posCenter, p, Position, Light; + float Scale = sinf(WorldTime * 0.004f) * 0.3f + 0.3f; + + Scale = (Scale * 10.f) + 20.f; + + Vector(0.6f, 0.3f, 0.8f, Light); + + Vector(0.f, 0.f, 0.f, p); + + for (int i = 0; i < 5; ++i) + { + b->TransformPosition(BoneTransform[22 - i], p, posCenter, true); + b->TransformPosition(BoneTransform[30 - i], p, Position, true); + if (rand_fps_check(1)) + { + CreateJoint(BITMAP_JOINT_THUNDER, Position, posCenter, o->Angle, 14, o, Scale); + CreateJoint(BITMAP_JOINT_SPIRIT, posCenter, Position, o->Angle, 4, o, Scale + 5); + } + + CreateSprite(BITMAP_FLARE_BLUE, posCenter, Scale / 28.f, Light, o); + } + + for (int i = 0; i < 5; ++i) + { + b->TransformPosition(BoneTransform[7 - i], p, posCenter, true); + b->TransformPosition(BoneTransform[11 + i], p, Position, true); + if (rand_fps_check(1)) + { + CreateJoint(BITMAP_JOINT_THUNDER, Position, posCenter, o->Angle, 14, o, Scale); + CreateJoint(BITMAP_JOINT_SPIRIT, posCenter, Position, o->Angle, 4, o, Scale + 5); + } + + CreateSprite(BITMAP_FLARE_BLUE, posCenter, Scale / 28.f, Light, o); + } + return Result::Applied; +} + +Result ApplyWingOfStorm(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + vec3_t vRelativePos, vPos, vLight; + Vector(0.f, 0.f, 0.f, vRelativePos); + Vector(0.f, 0.f, 0.f, vPos); + Vector(0.f, 0.f, 0.f, vLight); + + float fLuminosity = absf(sinf(WorldTime * 0.0004f)) * 0.4f; + Vector(0.5f + fLuminosity, 0.5f + fLuminosity, 0.5f + fLuminosity, vLight); + int iBone[] = {9, 20, 19, 10, 18, 28, 27, 36, 35, 38, 37, 53, 48, 62, 70, 72, 71, 78, 79, 80, 87, 90, 91, 106, 102}; + float fScale = 0.f; + + for (int i = 0; i < 25; ++i) + { + b->TransformPosition(BoneTransform[iBone[i]], vRelativePos, vPos, true); + fScale = 0.5f; // (rand()%10) * 0.05f + 0.3f; + CreateSprite(BITMAP_CLUD64, vPos, fScale, vLight, o, WorldTime * 0.01f, 1); + } + + int iBoneThunder[] = {11, 21, 29, 63, 81, 89}; + if (rand_fps_check(2)) + { + for (int i = 0; i < 6; ++i) + { + b->TransformPosition(BoneTransform[iBoneThunder[i]], vRelativePos, vPos, true); + if (rand_fps_check(20)) + { + Vector(0.6f, 0.6f, 0.9f, vLight); + CreateEffect(MODEL_FENRIR_THUNDER, vPos, o->Angle, vLight, 1, o); + } + } + } + + int iBoneLight[] = {64, 61, 69, 77, 86, 98, 97, 99, 104, 103, 105, 12, 8, 17, 26, 34, 52, 44, 51, 50, 49, 45}; + + fScale = absf(sinf(WorldTime * 0.003f)) * 0.2f; + + for (int i = 0; i < 22; ++i) + { + b->TransformPosition(BoneTransform[iBoneLight[i]], vRelativePos, vPos, true); + if (iBoneLight[i] == 12 || iBoneLight[i] == 64 || iBoneLight[i] == 98 || iBoneLight[i] == 52) + { + Vector(0.9f, 0.0f, 0.0f, vLight); + CreateSprite(BITMAP_LIGHT, vPos, fScale + 1.4f, vLight, o); + } + else + { + Vector(0.8f, 0.5f, 0.2f, vLight); + CreateSprite(BITMAP_LIGHT, vPos, fScale + 0.3f, vLight, o); + } + } + return Result::Applied; +} + +Result ApplyWingOfEternal(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + vec3_t p, Position, Light; + Vector(0.f, 0.f, 0.f, p); + float Scale = absf(sinf(WorldTime * 0.003f)) * 0.2f; + float Luminosity = absf(sinf(WorldTime * 0.003f)) * 0.3f; + + Vector(0.5f + Luminosity, 0.5f + Luminosity, 0.6f + Luminosity, Light); + // int iRedFlarePos[] = { 25, 32, 53, 15, 9, 35 }; + int iRedFlarePos[] = {24, 31, 15, 8, 53, 35}; + for (int i = 0; i < 6; ++i) + { + b->TransformPosition(BoneTransform[iRedFlarePos[i]], p, Position, true); + CreateSprite(BITMAP_LIGHT, Position, Scale + 1.3f, Light, o); + } + + Vector(0.1f, 0.1f, 0.9f, Light); + // int iGreenFlarePos[] = { 23, 22, 24, 34, 5, 31, 14, 12, 27, 8, 6, 7, 16, 13, 56, 37, 58, 40, 39, 38 }; + int iGreenFlarePos[] = {22, 23, 25, 29, 30, 28, 32, 13, 16, 14, 12, 9, 7, 6, 57, 58, 40, 39}; + + for (int i = 0; i < 18; ++i) + { + b->TransformPosition(BoneTransform[iGreenFlarePos[i]], p, Position, true); + CreateSprite(BITMAP_LIGHT, Position, Scale + 1.5f, Light, o); + } + int iGreenFlarePos2[] = {56, 38, 51, 45}; + + for (int i = 0; i < 4; ++i) + { + b->TransformPosition(BoneTransform[iGreenFlarePos2[i]], p, Position, true); + CreateSprite(BITMAP_LIGHT, Position, Scale + 0.5f, Light, o); + } + return Result::Applied; +} + +Result ApplyWingOfIllusion(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + vec3_t p, Position, Light; + Vector(0.f, 0.f, 0.f, p); + float Scale = absf(sinf(WorldTime * 0.002f)) * 0.2f; + float Luminosity = absf(sinf(WorldTime * 0.002f)) * 0.4f; + + Vector(0.5f + Luminosity, 0.0f + Luminosity, 0.0f + Luminosity, Light); + int iRedFlarePos[] = {5, 6, 7, 8, 18, 19, 23, 24, 25, 27, 37, 38}; + for (int i = 0; i < 12; ++i) + { + b->TransformPosition(BoneTransform[iRedFlarePos[i]], p, Position, true); + CreateSprite(BITMAP_FLARE, Position, Scale + 0.6f, Light, o); + } + + Vector(0.0f + Luminosity, 0.5f + Luminosity, 0.0f + Luminosity, Light); + int iGreenFlarePos[] = {4, 9, 13, 14, 26, 32, 31, 33}; + + for (int i = 0; i < 8; ++i) + { + b->TransformPosition(BoneTransform[iGreenFlarePos[i]], p, Position, true); + CreateSprite(BITMAP_LIGHT, Position, 1.3f, Light, o); + } + + Vector(1.0f, 1.0f, 1.0f, Light); + float fLumi = (sinf(WorldTime * 0.004f) + 1.0f) * 0.05f; + Vector(0.8f + fLumi, 0.8f + fLumi, 0.3f + fLumi, Light); + CreateSprite(BITMAP_LIGHT, Position, 0.4f, Light, o, 0.5f); + if (rand_fps_check(2)) + { + b->TransformPosition(BoneTransform[13], p, Position, true); + CreateParticle(BITMAP_SHINY, Position, o->Angle, Light, 5, 0.5f); + b->TransformPosition(BoneTransform[31], p, Position, true); + CreateParticle(BITMAP_SHINY, Position, o->Angle, Light, 5, 0.5f); + } + return Result::Applied; +} + +Result ApplyWingOfRuin(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + vec3_t p, Position, Light; + Vector(0.f, 0.f, 0.f, p); + float Scale = absf(sinf(WorldTime * 0.003f)) * 0.2f; + float Luminosity = absf(sinf(WorldTime * 0.003f)) * 0.3f; + + Vector(0.7f + Luminosity, 0.5f + Luminosity, 0.8f + Luminosity, Light); + int iRedFlarePos[] = {6, 15, 24, 56, 47, 38}; + for (int i = 0; i < 6; ++i) + { + b->TransformPosition(BoneTransform[iRedFlarePos[i]], p, Position, true); + CreateSprite(BITMAP_LIGHT, Position, Scale + 1.5f, Light, o); + } + + Vector(0.6f, 0.4f, 0.7f, Light); + int iSparkPos[] = {7, 16, 25, 57, 48, 39, 11, 22, 31, 63, 54, 40, 10, 21, 30, 62, 53, 41, 9, + 20, 29, 61, 52, 42, 8, 19, 28, 60, 51, 43, 18, 27, 59, 50, 17, 26, 58, 49}; + int iNumParticle = 1; + + for (int i = 0; i < 6; ++i) + { + b->TransformPosition(BoneTransform[iSparkPos[i]], p, Position, true); + for (int j = 0; j < iNumParticle; ++j) + if (rand_fps_check(1)) + CreateParticle(BITMAP_CHROME_ENERGY2, Position, o->Angle, Light, 0, 0.1f); + } + + for (int i = 6; i < 18; ++i) + { + b->TransformPosition(BoneTransform[iSparkPos[i]], p, Position, true); + for (int j = 0; j < iNumParticle; ++j) + if (rand_fps_check(1)) + CreateParticle(BITMAP_CHROME_ENERGY2, Position, o->Angle, Light, 0, 0.3f); + } + + for (int i = 18; i < 30; ++i) + { + b->TransformPosition(BoneTransform[iSparkPos[i]], p, Position, true); + for (int j = 0; j < iNumParticle; ++j) + if (rand_fps_check(1)) + CreateParticle(BITMAP_CHROME_ENERGY2, Position, o->Angle, Light, 0, 0.5f); + } + + for (int i = 30; i < 38; ++i) + { + b->TransformPosition(BoneTransform[iSparkPos[i]], p, Position, true); + for (int j = 0; j < iNumParticle; ++j) + if (rand_fps_check(1)) + CreateParticle(BITMAP_CHROME_ENERGY2, Position, o->Angle, Light, 0, 0.7f); + } + return Result::Applied; +} + +Result ApplyWingOfDimension(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) +{ + vec3_t p, Position, Light; + Vector(0.f, 0.f, 0.f, p); + float Scale = absf(sinf(WorldTime * 0.002f)) * 0.2f; + float Luminosity = absf(sinf(WorldTime * 0.002f)) * 0.4f; + + Vector((1.0f + Luminosity) / 2.f, (0.7f + Luminosity) / 2.f, (0.2f + Luminosity) / 2.f, Light); + int iFlarePos0[] = {7, 30, 31, 43, 8, 20}; + + int icnt; + for (icnt = 0; icnt < 2; ++icnt) + { + b->TransformPosition(BoneTransform[iFlarePos0[icnt]], p, Position, true); + CreateSprite(BITMAP_FLARE, Position, Scale + 2.0f, Light, o); + } + Vector((1.0f + Luminosity) / 4.f, (0.7f + Luminosity) / 4.f, (0.2f + Luminosity) / 4.f, Light); + for (; icnt < 6; ++icnt) + { + b->TransformPosition(BoneTransform[iFlarePos0[icnt]], p, Position, true); + CreateSprite(BITMAP_FLARE, Position, Scale + 0.5f, Light, o); + } + + Vector((0.5f + Luminosity) / 2.f, (0.1f + Luminosity) / 2.f, (0.4f + Luminosity) / 2.f, Light); + int iGreenFlarePos[] = {29, 38, 42, 19, 15, 6}; + + for (int i = 0; i < 6; ++i) + { + b->TransformPosition(BoneTransform[iGreenFlarePos[i]], p, Position, true); + CreateSprite(BITMAP_FLARE, Position, Scale + 2.0f, Light, o); + } + return Result::Applied; +} + +// ------------------------------------------------ the shine below +3 + +void RenderCursedCastleWater(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType, float* Light) +{ + RenderPartObjectBody(b, o, Type, Alpha, RenderType); + RenderPartObjectBodyColor2(b, o, Type, 0.5f, RENDER_TEXTURE | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 0.5f); + 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) +{ + VectorCopy(Light, b->BodyLight); + RenderPartObjectBody(b, o, Type, Alpha, RenderType); + RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); + RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); +} + +void RenderSealShine(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType, float* Light) +{ + Vector(Light[0] * 0.9f, Light[1] * 0.9f, Light[2] * 0.9f, b->BodyLight); + RenderPartObjectBody(b, o, Type, Alpha, RenderType); + RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); + RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); +} + +// ------------------------------------------------ the names + +const ItemEffect ItemEffects[] = { + {"billOfBalrog", ApplyBillOfBalrog}, + {"meshesPerLevel", ApplyMeshesPerLevel}, + {"firecracker", ApplyFirecracker}, + {"gmGift", ApplyGmGift}, + {"darkLordScroll", ApplyDarkLordScroll}, + {"spirit", ApplySpirit}, + {"potion", ApplyPotion}, + {"fruits", ApplyFruits}, + {"bloodBone", ApplyBloodBone}, + {"invisibilityCloak", ApplyInvisibilityCloak}, + {"devilsEye", ApplyDevilsEye}, + {"devilsKey", ApplyDevilsKey}, + {"devilsInvitation", ApplyDevilsInvitation}, + {"rena", ApplyRena}, + {"wingsOfDragon", ApplyWingsOfDragon}, + {"wingsOfSoul", ApplyWingsOfSoul}, + {"redSpirit", ApplyRedSpirit}, + {"staffOfKundun", ApplyStaffOfKundun}, + {"divineSet", ApplyDivineSet}, + {"hiddenMeshByLevel", ApplyHiddenMeshByLevel}, + {"hideMesh1", ApplyHideMesh1}, + {"wingsOfDarkness", ApplyWingsOfDarkness}, + {"wingOfStorm", ApplyWingOfStorm}, + {"wingOfEternal", ApplyWingOfEternal}, + {"wingOfIllusion", ApplyWingOfIllusion}, + {"wingOfRuin", ApplyWingOfRuin}, + {"wingOfDimension", ApplyWingOfDimension}, + {"cursedCastleWater", nullptr, RenderCursedCastleWater}, + {"harmonyShine", nullptr, RenderHarmonyShine}, + {"sealShine", nullptr, RenderSealShine}, +}; + +const ItemEffect* FindEffect(std::string_view name) +{ + const auto found = std::find_if(std::begin(ItemEffects), std::end(ItemEffects), + [name](const ItemEffect& effect) { return effect.name == name; }); + return found != std::end(ItemEffects) ? found : nullptr; +} + +// The effect of every item type, taken from the item model database after +// each build of it. +ItemModelTable g_itemEffects{[](const Data::Items::ItemModelDefinition& model) + { + // Names that are not effects are drawn without one; the + // model loader reports them. + return model.effect.empty() ? nullptr : FindEffect(model.effect); + }}; +} // namespace + +bool Exists(std::string_view name) +{ + return FindEffect(name) != nullptr; +} + +Result Apply(BMD* b, OBJECT* o, int modelType, float alpha, int& level) +{ + const ItemEffect* effect = g_itemEffects.Find(modelType); + if (effect == nullptr || effect->apply == nullptr) + { + return Result::None; + } + return effect->apply(b, o, modelType, alpha, level); +} + +bool RenderBelowPlus3(BMD* b, OBJECT* o, int modelType, float alpha, int renderType, float* light) +{ + const ItemEffect* effect = g_itemEffects.Find(modelType); + if (effect == nullptr || effect->renderBelowPlus3 == nullptr) + { + return false; + } + effect->renderBelowPlus3(b, o, modelType, alpha, renderType, light); + return true; +} +} // namespace Render::Items::Effects diff --git a/src/source/Render/Items/ItemEffects.h b/src/source/Render/Items/ItemEffects.h new file mode 100644 index 000000000..5999dbe27 --- /dev/null +++ b/src/source/Render/Items/ItemEffects.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +class BMD; +class OBJECT; + +// What item models do besides being drawn: sprites, particles and lightning +// on their bones, a pulsing glow mesh, a mesh hidden by level, their own +// drawing instead of the plain one, the shine of some items below +3. Each +// effect is code with a name; the model entry of an item names its effect +// (Data/Items/Models, "effect"). Items with the same effect share it. +namespace Render::Items::Effects +{ +// Whether there is an effect of this name. The model loader checks the names +// of the model entries with it. +bool Exists(std::string_view name); + +enum class Result +{ + // The item has no effect that runs before the model is drawn. + None, + // The effect ran; the model is drawn as usual. + Applied, + // The effect drew the model itself; nothing more is drawn. + Drawn, +}; + +// Runs the effect of the item before its model is drawn (the effects are +// taken from the item model database after each build of it). It may change +// the level the model glows like. +Result Apply(BMD* b, OBJECT* o, int modelType, float alpha, int& level); + +// Draws the model below +3 for the effects that shine then, instead of the +// plain drawing. False for the others. +bool RenderBelowPlus3(BMD* b, OBJECT* o, int modelType, float alpha, int renderType, float* light); +} // namespace Render::Items::Effects diff --git a/src/source/Render/Items/ItemModelLookup.h b/src/source/Render/Items/ItemModelLookup.h index 49ce15a61..92c3a351d 100644 --- a/src/source/Render/Items/ItemModelLookup.h +++ b/src/source/Render/Items/ItemModelLookup.h @@ -3,6 +3,9 @@ #include "Core/Globals/_enum.h" #include "Data/GameData/ItemData/ItemModelDatabase.h" +#include +#include + namespace Render::Items { // The item model entry of a model slot (MODEL_ITEM + item type), or nullptr @@ -26,4 +29,47 @@ inline int GetItemTypeOfModel(int modelType) const int itemType = modelType - MODEL_ITEM; return Data::Items::IsValidItemType(itemType) ? itemType : -1; } + +// 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 class ItemModelTable +{ +public: + using Take = T (*)(const Data::Items::ItemModelDefinition& model); + + explicit ItemModelTable(Take take) : m_take(take) {} + + // The value of the item a model slot is drawn like (GetItemTypeOfModel); + // T{} for models that are not items and for slots without a model. + T Find(int modelType) + { + const int itemType = GetItemTypeOfModel(modelType); + return itemType >= 0 ? Values()[itemType] : T{}; + } + +private: + const std::vector& Values() + { + const int databaseVersion = g_ItemModelDatabase.GetVersion(); + if (m_databaseVersion != databaseVersion) + { + m_databaseVersion = databaseVersion; + const std::span models = g_ItemModelDatabase.GetAllSlots(); + m_values.assign(models.size(), T{}); + for (size_t itemType = 0; itemType < models.size(); ++itemType) + { + if (models[itemType].Exists()) + { + m_values[itemType] = m_take(models[itemType]); + } + } + } + return m_values; + } + + Take m_take; + int m_databaseVersion = -1; + std::vector m_values; +}; } // namespace Render::Items diff --git a/src/source/Render/Items/ItemRenderStyles.cpp b/src/source/Render/Items/ItemRenderStyles.cpp index a13b2beba..49aa0b616 100644 --- a/src/source/Render/Items/ItemRenderStyles.cpp +++ b/src/source/Render/Items/ItemRenderStyles.cpp @@ -1824,16 +1824,6 @@ const RenderStyle RenderStyles[] = { {"luckyItem", RenderLuckyItem}, }; -// The render style of every item type, taken from the item model database -// after each build of it. -struct ItemStyleTable -{ - int databaseVersion = -1; - std::vector styles; -}; - -ItemStyleTable g_itemStyles; - const RenderStyle* FindStyle(std::string_view name) { const auto found = std::find_if(std::begin(RenderStyles), std::end(RenderStyles), @@ -1841,32 +1831,15 @@ const RenderStyle* FindStyle(std::string_view name) return found != std::end(RenderStyles) ? found : nullptr; } -const std::vector& GetItemStyles() -{ - const int databaseVersion = g_ItemModelDatabase.GetVersion(); - if (g_itemStyles.databaseVersion != databaseVersion) - { - g_itemStyles.databaseVersion = databaseVersion; - g_itemStyles.styles.assign(MAX_ITEM, nullptr); - const std::span models = g_ItemModelDatabase.GetAllSlots(); - for (size_t itemType = 0; itemType < models.size(); ++itemType) - { - // Names that are not styles are drawn plainly; the model loader - // reports them. - if (models[itemType].Exists() && !models[itemType].renderStyle.empty()) - { - g_itemStyles.styles[itemType] = FindStyle(models[itemType].renderStyle); - } - } - } - return g_itemStyles.styles; -} - -const RenderStyle* FindItemStyle(int modelType) -{ - const int itemType = GetItemTypeOfModel(modelType); - return itemType >= 0 ? GetItemStyles()[itemType] : nullptr; -} +// The render style of every item type, taken from the item model database +// after each build of it. +ItemModelTable g_itemStyles{[](const Data::Items::ItemModelDefinition& model) + { + // Names that are not styles are drawn plainly; the model + // loader reports them. + return model.renderStyle.empty() ? nullptr + : FindStyle(model.renderStyle); + }}; } // namespace bool Exists(std::string_view name) @@ -1876,13 +1849,13 @@ bool Exists(std::string_view name) bool Render(BMD* b, OBJECT* o, int modelType, float alpha, int renderType) { - const RenderStyle* style = FindItemStyle(modelType); + const RenderStyle* style = g_itemStyles.Find(modelType); return style != nullptr && style->Render(b, o, modelType, alpha, renderType); } bool RenderGlow(BMD* b, OBJECT* o, int modelType, float alpha, int renderType, int texture) { - const RenderStyle* style = FindItemStyle(modelType); + const RenderStyle* style = g_itemStyles.Find(modelType); if (style == nullptr || style->glow == nullptr) { return false; diff --git a/tests/data/test_item_model_files.cpp b/tests/data/test_item_model_files.cpp index 51ae9a587..719d4cc2d 100644 --- a/tests/data/test_item_model_files.cpp +++ b/tests/data/test_item_model_files.cpp @@ -13,8 +13,9 @@ #include "Data/GameData/ItemData/ItemModelJsonFormat.h" #include "Data/GameData/ItemData/ItemTextureFiles.h" #include "Data/GameData/ItemData/ItemType.h" -#include "Render/Items/ItemDisplay.h" #include "GameLogic/Social/MonkSystem.h" +#include "Render/Items/ItemDisplay.h" +#include "Render/Items/ItemEffects.h" #include "Render/Items/ItemGlow.h" #include "Render/Items/ItemModelLookup.h" #include "Render/Items/ItemRenderStyles.h" @@ -709,3 +710,105 @@ TEST_CASE("Styles that pick something per item draw other items plainly [data][i object.Type = MODEL_ITEM + MakeItemType(12, 65); CHECK_FALSE(Styles::Render(&model, &object, object.Type, 1.f, RENDER_TEXTURE)); } + +TEST_CASE("The effects of shipped item models exist [data][items]") +{ + for (const ItemModelDefinition& model : ShippedModels().models) + { + if (!model.effect.empty()) + { + INFO("(" << model.group << "," << model.number << ") " << model.effect); + CHECK(Render::Items::Effects::Exists(model.effect)); + } + } + CHECK_FALSE(Render::Items::Effects::Exists("wingOfEternl")); +} + +// The effects of the old drawing code (RenderPartObjectEffect), recorded per +// item: spot checks. +TEST_CASE("Shipped item models keep the effects of the old drawing code [data][items]") +{ + const std::vector& models = ShippedModels().models; + const auto effectOf = [&](int group, int number) + { + const ItemModelDefinition* model = FindModel(models, group, number); + REQUIRE(model != nullptr); + return model->effect; + }; + + CHECK(effectOf(0, 0).empty()); + CHECK(effectOf(12, 37) == "wingOfEternal"); + CHECK(effectOf(14, 18) == "devilsKey"); + CHECK(effectOf(14, 19) == "devilsInvitation"); + CHECK(effectOf(14, 0) == "potion"); + CHECK(effectOf(14, 6) == "potion"); + // Items with the same code share it. + CHECK(effectOf(14, 7) == "hiddenMeshByLevel"); + CHECK(effectOf(13, 7) == "hiddenMeshByLevel"); + // The shine below +3. + CHECK(effectOf(13, 43) == "sealShine"); + CHECK(effectOf(13, 94) == "sealShine"); + CHECK(effectOf(14, 42) == "harmonyShine"); + CHECK(effectOf(13, 50) == "harmonyShine"); + CHECK(effectOf(14, 64) == "cursedCastleWater"); + + // The socket seeds and spheres and zen glow like level 0, whatever their + // level (the old code set their level to 0, or drew zen plainly). + g_ItemModelDatabase.Build(models, Data::Effects::GlowColorList{}); + for (const int itemType : + {MakeItemType(12, 60), MakeItemType(12, 100), MakeItemType(12, 129), MakeItemType(14, 15)}) + { + INFO("item type " << itemType); + CHECK(Render::Items::Glow::GetLevel(MODEL_ITEM + itemType, 9) == 0); + } + g_ItemModelDatabase.Build({}, Data::Effects::GlowColorList{}); +} + +TEST_CASE("Item effects run before the model is drawn [data][items]") +{ + using namespace Render::Items; + using Effects::Result; + g_ItemModelDatabase.Build(ShippedModels().models, Data::Effects::GlowColorList{}); + + // A model without meshes draws nothing, so only the values change here. + BMD model; + OBJECT object; + const auto apply = [&](int itemType, int& level) + { + object.Type = MODEL_ITEM + itemType; + return Effects::Apply(&model, &object, object.Type, 1.f, level); + }; + + // Potions with a level glow like +7. + int level = 3; + CHECK(apply(MakeItemType(14, 0), level) == Result::Applied); + CHECK(level == 7); + level = 0; + CHECK(apply(MakeItemType(14, 0), level) == Result::Applied); + CHECK(level == 0); + // The siege potion hides a mesh by level. + level = 0; + CHECK(apply(MakeItemType(14, 7), level) == Result::Applied); + CHECK(object.HiddenMesh == 1); + level = 1; + CHECK(apply(MakeItemType(14, 7), level) == Result::Applied); + CHECK(object.HiddenMesh == 0); + // Some effects draw the model themselves. + level = 2; + CHECK(apply(MakeItemType(14, 27), level) == Result::Drawn); + // Items without an effect, and models that are not items. + CHECK(apply(ITEM_KRIS, level) == Result::None); + object.Type = MODEL_PLAYER; + CHECK(Effects::Apply(&model, &object, MODEL_PLAYER, 1.f, level) == Result::None); + + // The shine below +3 of the seals; other items are drawn plainly. + vec3_t light = {1.f, 1.f, 1.f}; + object.Type = MODEL_ITEM + ITEM_KRIS; + CHECK_FALSE(Effects::RenderBelowPlus3(&model, &object, object.Type, 1.f, RENDER_TEXTURE, light)); + object.Type = MODEL_ITEM + MakeItemType(13, 43); + CHECK(Effects::RenderBelowPlus3(&model, &object, object.Type, 1.f, RENDER_TEXTURE, light)); + + // The effects follow the database when it is built again. + g_ItemModelDatabase.Build({}, Data::Effects::GlowColorList{}); + CHECK(apply(MakeItemType(14, 0), level) == Result::None); +} diff --git a/tests/data/test_item_model_json.cpp b/tests/data/test_item_model_json.cpp index f93f09bc4..0b71f87c3 100644 --- a/tests/data/test_item_model_json.cpp +++ b/tests/data/test_item_model_json.cpp @@ -53,6 +53,7 @@ ItemModelDefinition MakeDarkHorse() model.textureFolders = {"Item", "Skill"}; model.noneBlendMeshes = {1, 3}; model.renderStyle = "darkHorse"; + model.effect = "darkHorseAura"; return model; } } // namespace @@ -171,6 +172,23 @@ TEST_CASE("The render style of a model is a name [data][items]") CHECK(result.models[0].renderStyle.empty()); } +TEST_CASE("The effect of a model is a name [data][items]") +{ + const std::string text = WriteItemModelGroupJson(13, std::vector{MakeDarkHorse()}); + CHECK(text.find(R"("effect": "darkHorseAura")") != std::string::npos); + // The effect comes after the render style. + CHECK(text.find(R"("renderStyle")") < text.find(R"("effect")")); + + CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "effect": ["sparks"]})", "effect")); + CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "effect": "red sparks"})", "effect")); + CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "effect": ""})", "effect")); + + // Without an effect the model is only drawn. + const ReadResult result = Read(GroupFile(13, R"({"number": 8, "file": "Data/Item/Ring01.bmd"})")); + REQUIRE(result.models.size() == 1); + CHECK(result.models[0].effect.empty()); +} + TEST_CASE("A model with errors is not read [data][items]") { const ReadResult result = diff --git a/tests/data/test_item_model_problems.cpp b/tests/data/test_item_model_problems.cpp index f4009ffc1..00152f527 100644 --- a/tests/data/test_item_model_problems.cpp +++ b/tests/data/test_item_model_problems.cpp @@ -140,7 +140,7 @@ TEST_CASE("A render style that does not exist is a warning [data][items]") { ItemModelProblem problem = MakeMissingTexture(0, ""); problem.type = ItemModelProblemType::RenderStyleUnknown; - problem.renderStyle = "stormCorw"; + problem.name = "stormCorw"; // The item keeps its model and textures; only its look is lost. CHECK_FALSE(problem.IsError()); @@ -148,6 +148,17 @@ TEST_CASE("A render style that does not exist is a warning [data][items]") "(0,33): render style stormCorw does not exist; Data/Item/Sword34.bmd is drawn plainly " + SwordModels); } +TEST_CASE("An effect that does not exist is a warning [data][items]") +{ + ItemModelProblem problem = MakeMissingTexture(0, ""); + problem.type = ItemModelProblemType::EffectUnknown; + problem.name = "wingOfEternl"; + + CHECK_FALSE(problem.IsError()); + CHECK(problem.ToLogString() == + "(0,33): effect wingOfEternl does not exist; Data/Item/Sword34.bmd is drawn without it " + SwordModels); +} + TEST_CASE("The item model error message lists the errors only, up to a limit [data][items]") { const ItemDatabase items = MakeItems(); From 121e0c408bab3b6b284c7c3b58db44a88a032aee Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Wed, 30 Sep 2026 01:35:27 +0200 Subject: [PATCH 5/9] fix(items): review of the item effects (phase 4c3) - 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". --- docs/item-data.md | 31 ++++-- .../2026-09-25-data-driven-items-design.md | 22 ++-- .../UI/ItemEditor/ItemEditorLooks.cpp | 104 ++++++++++++++---- src/MuEditor/UI/ItemEditor/ItemEditorLooks.h | 4 +- .../UI/ItemEditor/ItemEditorTable.cpp | 63 +++++++---- src/MuEditor/UI/ItemEditor/ItemEditorTable.h | 7 +- src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp | 20 +++- src/MuEditor/UI/ItemEditor/MuItemEditorUI.h | 3 + src/bin/Data/Items/Models/Group13_Helper.json | 23 ++-- src/bin/Data/Items/Models/Group14_Potion.json | 5 +- .../DataHandler/ItemData/ItemModelLoader.h | 10 +- .../GameData/ItemData/ItemModelDefinition.h | 2 +- src/source/Engine/Object/ZzzObject.cpp | 39 +++++-- src/source/Engine/Object/ZzzObject.h | 6 + src/source/Render/Items/ItemEffects.cpp | 69 ++---------- src/source/Render/Items/ItemEffects.h | 10 +- src/source/Render/Items/ItemModelLookup.h | 46 -------- src/source/Render/Items/ItemModelTable.h | 54 +++++++++ src/source/Render/Items/ItemRenderStyles.cpp | 49 +++++++-- src/source/Render/Items/ItemRenderStyles.h | 23 ++++ tests/data/test_item_model_files.cpp | 95 +++++++++++++--- 21 files changed, 451 insertions(+), 234 deletions(-) create mode 100644 src/source/Render/Items/ItemModelTable.h diff --git a/docs/item-data.md b/docs/item-data.md index d3754fbae..f85bbeaa5 100644 --- a/docs/item-data.md +++ b/docs/item-data.md @@ -246,7 +246,7 @@ item files hold what client and server share. | `ground` | How the item lies on the ground, see below. Optional. | | `glow` | How the item glows, see below. Optional. | | `renderStyle` | The look of the model when it is more than a plain textured model, see below. Optional. | -| `effect` | What the model does besides being drawn (sprites and particles on its bones, a pulsing glow mesh, ...), see below. Optional. | +| `effect` | What the model does before it is drawn (sprites and particles on its bones, a pulsing glow mesh, ...), see below. Optional. | | `cloth` | `true` for capes that are worn as cloth: when one is put on or taken off, the character's cloth is deleted, so the next cape builds its own. Optional. The flag does not make a cape cloth; which capes are drawn as cloth, and how, is still decided in code. | `inventory` and `ground` hold these values; a missing value has the @@ -340,7 +340,13 @@ only in the hands of the Metal Balrog and the Orc Archer of Doom, `helperNpcPlate` only on the helper NPCs (Luke and Leo the Helper, Helper Ellen; in the code the flag of a PC room look, which no player gets). -`effect` names what the model does besides being drawn: +A few styles also shine below +3: the model is drawn with the light of the +item scaled, then with two shine passes. `sealOfAscension`, `sealOfWealth` +and `sealOfSustenance` (the seals, light at 0.9), `illusionSorcererCovenant` +and `harmonyShine` (the Jewel of Harmony and the Moonstone Pendant, which +are drawn plainly otherwise), and `cursedCastleWater`. + +`effect` names what the model does before it is drawn: ```json { "number": 37, "file": "Data/Item/wing09.bmd", "textureFolders": ["Item"], "effect": "wingOfEternal" } @@ -356,16 +362,15 @@ is drawn and can: - 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); + `hideMesh1`), the level potions glow like (`potion`: +7 at every level + above 0); - draw the model itself instead of the usual drawing (the Dark Lord's scrolls, `fruits`, `spirit`, `bloodBone`, `invisibilityCloak`, `firecracker`, `gmGift`, `meshesPerLevel`). -Three effects instead give items their own shine below +3: -`harmonyShine` (Jewel of Harmony, Moonstone Pendant, Illusion Sorcerer -Covenant), `sealShine` (the seals) and `cursedCastleWater`. Items with the -same effect share it. The socket seeds and spheres and zen have no effect; -they glow like level 0 (`"glow": {"level": 0}`), whatever their level. +Items with the same effect share it. The socket seeds and spheres and zen +have no effect; they glow like level 0 (`"glow": {"level": 0}`), whatever +their level. The effects of the event models that level variants are drawn with stay in code; they get model entries later. @@ -449,6 +454,16 @@ The game runs from the build folder, which has a copy of `src/bin/Data`. To keep your changes, copy the changed files from `/Data/Items` to `src/bin/Data/Items` and commit them. +### Looks + +The **Looks** section above the item table shows the model data of the +selected item (`Data/Items/Models`), read only: its model file, its glow +values, its render style and its effect. A render style or an effect +opens to the list of all items that use it; clicking one selects it in +the table (the search is cleared when it hides that item). Items without +item data are listed too; they are not in the table, but selecting one +shows its looks. Changing the looks is done in the model files for now. + ### Import from bmd / Export as bmd The repository does not ship `Item_.bmd` files; the JSON files are the diff --git a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md index 9dc337abf..ea013549c 100644 --- a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md +++ b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md @@ -590,17 +590,21 @@ server with original clients (after phases 6 and B). loaded their models); their recipe (textured, then `RENDER_BRIGHT | RENDER_CHROME2`) comes back as a style with their model entries. - - **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 + - **4c3 Item effects:** `effect` names what the model does before it + is drawn, from the type chain of `RenderPartObjectEffect`: 29 item + branches for 81 items; 28 of them become 27 effects for 42 items in `Render/Items/ItemEffects.cpp` (moved unchanged; the Siege Potion and - the Contract share one), and the shine of 10 items below +3 becomes - three more (`harmonyShine`, `sealShine`, `cursedCastleWater`). An - effect places sprites, particles and lightning on bones, 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 socket - seeds and spheres and zen need no effect: they glow like level 0 + the Contract share one). An effect places sprites, particles and + lightning on bones, 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 socket seeds and spheres (the 39 items of the + last branch) and zen need no effect: they glow like level 0 (`"glow": {"level": 0}`), which is what their level checks did. The + shine of 10 items below +3 (two more shine passes) becomes values of + their render style, which the drawing code applies: 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` (138 styles now). The effects of the event models of level variants stay in `RenderPartObjectEffect` (no model entries yet). Which values the effects take (bones, colors, sizes) comes with phase 13, when what diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp index 20624a537..f9a3dfc7d 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp @@ -3,7 +3,6 @@ #ifdef _EDITOR #include "ItemEditorLooks.h" -#include "ItemEditorTable.h" #include "Data/GameData/ItemData/ItemDatabase.h" #include "Data/GameData/ItemData/ItemModelDatabase.h" @@ -11,14 +10,40 @@ #include "I18N/All.h" #include "imgui.h" +#include #include #include +#include +#include namespace { using Data::Items::ItemModelDefinition; using LookName = std::string ItemModelDefinition::*; +// At most this many items are listed at once; the list scrolls. +constexpr int MaxListedRows = 8; + +struct Look +{ + std::string name; + // The items that use the look: item type and log name. + std::vector> users; +}; + +// What the section shows for the selected item, built when the selection or +// the item model data changes (not every frame). +struct LooksCache +{ + int itemType = -1; + int databaseVersion = -1; + std::string glow; + Look renderStyle; + Look effect; +}; + +LooksCache g_cache; + // The glow values as the model file has them, or "-" without any. std::string DescribeGlow(const ItemModelDefinition& model) { @@ -28,58 +53,93 @@ std::string DescribeGlow(const ItemModelDefinition& model) return glow != json.end() ? glow->dump() : "-"; } -// A look of the item with the number of items that use it; opened, the list -// of those items. Clicking one selects it in the table. -void RenderLook(const char* label, const ItemModelDefinition& model, LookName look) +Look TakeLook(const ItemModelDefinition& model, LookName look) { - const std::string& name = model.*look; - if (name.empty()) + Look result{model.*look, {}}; + if (result.name.empty()) + { + return result; + } + const std::span models = g_ItemModelDatabase.GetAllSlots(); + for (int itemType = 0; itemType < static_cast(models.size()); ++itemType) + { + if (models[itemType].Exists() && models[itemType].*look == result.name) + { + result.users.emplace_back(itemType, g_ItemDatabase.GetLogName(itemType)); + } + } + return result; +} + +void Refresh(int itemType, const ItemModelDefinition& model) +{ + const int databaseVersion = g_ItemModelDatabase.GetVersion(); + if (g_cache.itemType == itemType && g_cache.databaseVersion == databaseVersion) { - ImGui::Text("%s: -", label); return; } + g_cache.itemType = itemType; + g_cache.databaseVersion = databaseVersion; + g_cache.glow = DescribeGlow(model); + g_cache.renderStyle = TakeLook(model, &ItemModelDefinition::renderStyle); + g_cache.effect = TakeLook(model, &ItemModelDefinition::effect); +} - const std::span models = g_ItemModelDatabase.GetAllSlots(); - const auto usesLook = [&](const ItemModelDefinition& other) { return other.Exists() && other.*look == name; }; - int users = 0; - for (const ItemModelDefinition& other : models) +// A look with the number of items that use it; opened, the list of those +// items in a box of fixed height. Returns the item clicked, -1 for none. +int RenderLook(const char* label, const Look& look) +{ + if (look.name.empty()) { - users += usesLook(other) ? 1 : 0; + ImGui::Text("%s: -", label); + return -1; } + int clicked = -1; ImGui::PushID(label); - if (ImGui::TreeNode("look", "%s: %s (%s %d)", label, name.c_str(), I18N::Editor::UsedBy, users)) + if (ImGui::TreeNode("look", "%s: %s (%s %d)", label, look.name.c_str(), I18N::Editor::UsedBy, + static_cast(look.users.size()))) { - for (int itemType = 0; itemType < static_cast(models.size()); ++itemType) + const int rows = std::min(static_cast(look.users.size()), MaxListedRows); + const ImVec2 size(-FLT_MIN, + ImGui::GetTextLineHeightWithSpacing() * rows + ImGui::GetStyle().FramePadding.y * 2.0f); + if (ImGui::BeginListBox("##users", size)) { - if (usesLook(models[itemType]) && ImGui::Selectable(g_ItemDatabase.GetLogName(itemType).c_str(), false)) + for (const auto& [itemType, name] : look.users) { - CItemEditorTable::RequestScrollToIndex(itemType); + if (ImGui::Selectable(name.c_str(), itemType == g_cache.itemType)) + { + clicked = itemType; + } } + ImGui::EndListBox(); } ImGui::TreePop(); } ImGui::PopID(); + return clicked; } } // namespace -void CItemEditorLooks::Render(int itemType) +int CItemEditorLooks::Render(int itemType) { if (!ImGui::CollapsingHeader(I18N::Editor::Looks)) { - return; + return -1; } const ItemModelDefinition* model = g_ItemModelDatabase.Find(itemType); if (model == nullptr) { ImGui::TextDisabled("%s", itemType < 0 ? I18N::Editor::SelectAnItem : I18N::Editor::NoModel); - return; + return -1; } + Refresh(itemType, *model); ImGui::Text("%s: %s", I18N::Editor::ModelFile, model->file.c_str()); - ImGui::Text("%s: %s", I18N::Editor::Glow, DescribeGlow(*model).c_str()); - RenderLook(I18N::Editor::RenderStyle, *model, &ItemModelDefinition::renderStyle); - RenderLook(I18N::Editor::Effect, *model, &ItemModelDefinition::effect); + ImGui::Text("%s: %s", I18N::Editor::Glow, g_cache.glow.c_str()); + const int clickedStyleUser = RenderLook(I18N::Editor::RenderStyle, g_cache.renderStyle); + const int clickedEffectUser = RenderLook(I18N::Editor::Effect, g_cache.effect); + return clickedStyleUser >= 0 ? clickedStyleUser : clickedEffectUser; } #endif // _EDITOR diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h index 394995ef7..629889cc1 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h @@ -8,7 +8,9 @@ class CItemEditorLooks { public: // Shown above the item table; `itemType` is the selected item, -1 for none. - static void Render(int itemType); + // Returns the item clicked in a list of the items that share a look, -1 + // for none. + static int Render(int itemType); }; #endif // _EDITOR diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorTable.cpp b/src/MuEditor/UI/ItemEditor/ItemEditorTable.cpp index 462fa7263..85816491a 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorTable.cpp +++ b/src/MuEditor/UI/ItemEditor/ItemEditorTable.cpp @@ -24,6 +24,23 @@ void CItemEditorTable::RequestScrollToIndex(int index) s_scrollToIndex = index; } +bool CItemEditorTable::IsListed(int itemIndex, const std::string& searchFilter) +{ + char nameBuffer[256]; + WideCharToMultiByte(CP_UTF8, 0, ItemAttribute[itemIndex].Name, -1, nameBuffer, sizeof(nameBuffer), NULL, NULL); + if (nameBuffer[0] == '\0') + { + return false; + } + if (searchFilter.empty()) + { + return true; + } + std::string nameLower = nameBuffer; + std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower); + return nameLower.find(searchFilter) != std::string::npos; +} + void CItemEditorTable::InvalidateFilter() { m_isInitialized = false; @@ -86,20 +103,10 @@ void CItemEditorTable::Render( m_filteredItems.clear(); for (int i = 0; i < MAX_ITEM; i++) { - char nameBuffer[256]; - WideCharToMultiByte(CP_UTF8, 0, ItemAttribute[i].Name, -1, nameBuffer, sizeof(nameBuffer), NULL, NULL); - - if (nameBuffer[0] == '\0') continue; - - if (searchFilter.length() > 0) + if (IsListed(i, searchFilter)) { - std::string nameLower = nameBuffer; - std::transform(nameLower.begin(), nameLower.end(), nameLower.begin(), ::tolower); - if (nameLower.find(searchFilter) == std::string::npos) - continue; + m_filteredItems.push_back(i); } - - m_filteredItems.push_back(i); } m_lastSearchFilter = searchFilter; m_isInitialized = true; @@ -109,8 +116,14 @@ void CItemEditorTable::Render( ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(2, 2)); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(4, 2)); - // Create a table with scrolling - if (!ImGui::BeginTable("ItemTable", visibleColumnCount, ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY | ImGuiTableFlags_ScrollX | ImGuiTableFlags_Resizable)) + // Create a table with scrolling; it keeps some rows when the parts above it + // (the Looks section) take the height of the window. + const float minTableHeight = ImGui::GetFrameHeightWithSpacing() * 10.0f; + const ImVec2 tableSize(0.0f, std::max(ImGui::GetContentRegionAvail().y, minTableHeight)); + if (!ImGui::BeginTable("ItemTable", visibleColumnCount, + ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg | ImGuiTableFlags_ScrollY | + ImGuiTableFlags_ScrollX | ImGuiTableFlags_Resizable, + tableSize)) { ImGui::PopStyleVar(2); return; @@ -156,20 +169,18 @@ void CItemEditorTable::Render( ImGuiListClipper clipper; clipper.Begin((int)m_filteredItems.size()); - // Handle scroll request + // Handle scroll request: the item is selected, and the table scrolls to its + // row when the search lists it. + int scrollRow = -1; if (s_scrollToIndex >= 0) { - // Find the row index in filtered items - for (int row = 0; row < (int)m_filteredItems.size(); row++) + const auto found = std::find(m_filteredItems.begin(), m_filteredItems.end(), s_scrollToIndex); + if (found != m_filteredItems.end()) { - if (m_filteredItems[row] == s_scrollToIndex) - { - // Force the clipper to include this row and scroll to it - ImGui::SetScrollY(ImGui::GetTextLineHeightWithSpacing() * (row + 1)); // +1 for header - selectedRow = s_scrollToIndex; - break; - } + scrollRow = static_cast(found - m_filteredItems.begin()); + clipper.IncludeItemByIndex(scrollRow); } + selectedRow = s_scrollToIndex; s_scrollToIndex = -1; // Reset } @@ -179,6 +190,10 @@ void CItemEditorTable::Render( { int i = m_filteredItems[row]; ImGui::TableNextRow(); + if (row == scrollRow) + { + ImGui::SetScrollHereY(0.5f); + } if (selectedRow == i) { diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorTable.h b/src/MuEditor/UI/ItemEditor/ItemEditorTable.h index 02db95ef7..4277a0ffd 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorTable.h +++ b/src/MuEditor/UI/ItemEditor/ItemEditorTable.h @@ -23,9 +23,14 @@ class CItemEditorTable int& selectedRow, bool freezeColumns); - // Request scroll to a specific item index + // Selects the item and scrolls to it at the next Render; the selection is + // set even when the search filter hides the item. static void RequestScrollToIndex(int index); + // Whether the table lists the item with this (lowercase) search filter: + // items with a name that contains it. + static bool IsListed(int itemIndex, const std::string& searchFilter); + // Force rebuild of filtered list (call after data changes) void InvalidateFilter(); diff --git a/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp b/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp index f5faa1617..19d7778e9 100644 --- a/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp +++ b/src/MuEditor/UI/ItemEditor/MuItemEditorUI.cpp @@ -158,8 +158,13 @@ void CMuItemEditorUI::Render(bool& showEditor) ImGui::Checkbox(I18N::Editor::FreezeIndexName, &m_bFreezeColumns); ImGui::Separator(); - // The looks of the selected item (read only). - CItemEditorLooks::Render(m_selectedRow); + // The looks of the selected item (read only); clicking an item there + // selects it. + const int clickedItem = CItemEditorLooks::Render(m_selectedRow); + if (clickedItem >= 0) + { + SelectItem(clickedItem); + } // Convert search to lowercase for case-insensitive search std::string searchLower = m_szItemSearchBuffer; @@ -177,6 +182,17 @@ void CMuItemEditorUI::Render(bool& showEditor) ImGui::End(); } +void CMuItemEditorUI::SelectItem(int itemIndex) +{ + std::string searchLower = m_szItemSearchBuffer; + std::transform(searchLower.begin(), searchLower.end(), searchLower.begin(), ::tolower); + if (!CItemEditorTable::IsListed(itemIndex, searchLower) && CItemEditorTable::IsListed(itemIndex, "")) + { + m_szItemSearchBuffer[0] = '\0'; + } + CItemEditorTable::RequestScrollToIndex(itemIndex); +} + void CMuItemEditorUI::RenderSearchBar() { // Search bar diff --git a/src/MuEditor/UI/ItemEditor/MuItemEditorUI.h b/src/MuEditor/UI/ItemEditor/MuItemEditorUI.h index 630c0b2c7..da0948276 100644 --- a/src/MuEditor/UI/ItemEditor/MuItemEditorUI.h +++ b/src/MuEditor/UI/ItemEditor/MuItemEditorUI.h @@ -23,6 +23,9 @@ class CMuItemEditorUI ~CMuItemEditorUI(); void RenderSearchBar(); + // Selects the item and shows it in the table; clears the search when it + // hides the item. + void SelectItem(int itemIndex); void RenderColumnVisibilityMenu(); char m_szItemSearchBuffer[256]; diff --git a/src/bin/Data/Items/Models/Group13_Helper.json b/src/bin/Data/Items/Models/Group13_Helper.json index 140ac1901..1c6c56801 100644 --- a/src/bin/Data/Items/Models/Group13_Helper.json +++ b/src/bin/Data/Items/Models/Group13_Helper.json @@ -402,7 +402,7 @@ "offset": [0, 0.02], "rotation": [-198, 0, 0] }, - "effect": "harmonyShine" + "renderStyle": "harmonyShine" }, { "number": 39, @@ -466,8 +466,7 @@ "rotation": [90, 0, -45], "scale": 0.5 }, - "renderStyle": "sealOfAscension", - "effect": "sealShine" + "renderStyle": "sealOfAscension" }, { "number": 44, @@ -483,8 +482,7 @@ "rotation": [90, 0, -45], "scale": 0.5 }, - "renderStyle": "sealOfWealth", - "effect": "sealShine" + "renderStyle": "sealOfWealth" }, { "number": 45, @@ -500,8 +498,7 @@ "rotation": [90, 0, -45], "scale": 0.5 }, - "renderStyle": "sealOfSustenance", - "effect": "sealShine" + "renderStyle": "sealOfSustenance" }, { "number": 46, @@ -584,8 +581,7 @@ "level": 0, "shineHiddenMesh": 1 }, - "renderStyle": "illusionSorcererCovenant", - "effect": "harmonyShine" + "renderStyle": "illusionSorcererCovenant" }, { "number": 51, @@ -1171,8 +1167,7 @@ "rotation": [0, 0, 90], "scale": 0.5 }, - "renderStyle": "sealOfAscension", - "effect": "sealShine" + "renderStyle": "sealOfAscension" }, { "number": 94, @@ -1184,8 +1179,7 @@ "rotation": [-90, 0, 0], "scale": 0.0021 }, - "renderStyle": "sealOfWealth", - "effect": "sealShine" + "renderStyle": "sealOfWealth" }, { "number": 97, @@ -1415,8 +1409,7 @@ "rotation": [90, 0, -45], "scale": 0.5 }, - "renderStyle": "sealOfWealth", - "effect": "sealShine" + "renderStyle": "sealOfWealth" }, { "number": 121, diff --git a/src/bin/Data/Items/Models/Group14_Potion.json b/src/bin/Data/Items/Models/Group14_Potion.json index a29bd41cc..d03f8d9e9 100644 --- a/src/bin/Data/Items/Models/Group14_Potion.json +++ b/src/bin/Data/Items/Models/Group14_Potion.json @@ -503,7 +503,7 @@ "glow": { "level": 0 }, - "effect": "harmonyShine" + "renderStyle": "harmonyShine" }, { "number": 43, @@ -788,8 +788,7 @@ "level": 0, "shineHiddenMesh": 0 }, - "renderStyle": "cursedCastleWater", - "effect": "cursedCastleWater" + "renderStyle": "cursedCastleWater" }, { "number": 65, diff --git a/src/source/Data/DataHandler/ItemData/ItemModelLoader.h b/src/source/Data/DataHandler/ItemData/ItemModelLoader.h index a0dae11a8..5a7ab8135 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelLoader.h +++ b/src/source/Data/DataHandler/ItemData/ItemModelLoader.h @@ -11,12 +11,18 @@ namespace Data::Items::ModelLoader // Whether a look of this name exists. using LookExists = bool (*)(std::string_view name); +// No look exists: a check that is left out reports all names. +inline bool NoLookExists(std::string_view) +{ + return false; +} + // The looks that exist; they are drawing code (Render::Items::Styles::Exists, // Render::Items::Effects::Exists). struct LookNames { - LookExists renderStyle; - LookExists effect; + LookExists renderStyle = NoLookExists; + LookExists effect = NoLookExists; }; // Opens the .bmd file of every item model; `lookNames` checks the names of diff --git a/src/source/Data/GameData/ItemData/ItemModelDefinition.h b/src/source/Data/GameData/ItemData/ItemModelDefinition.h index e140f6f27..7d00b1afb 100644 --- a/src/source/Data/GameData/ItemData/ItemModelDefinition.h +++ b/src/source/Data/GameData/ItemData/ItemModelDefinition.h @@ -127,7 +127,7 @@ struct ItemModelDefinition // The look of the model when it is more than a plain textured model: the // name of a render style (Render/Items/ItemRenderStyles). Empty: plain. std::string renderStyle; - // What the model does besides being drawn (sprites and particles on its + // What the model does before it is drawn (sprites and particles on its // bones, a pulsing glow mesh, its own drawing, ...): the name of an item // effect (Render/Items/ItemEffects). Empty: none. std::string effect; diff --git a/src/source/Engine/Object/ZzzObject.cpp b/src/source/Engine/Object/ZzzObject.cpp index c8b28e326..3b5d68dea 100644 --- a/src/source/Engine/Object/ZzzObject.cpp +++ b/src/source/Engine/Object/ZzzObject.cpp @@ -6724,6 +6724,16 @@ static Render::Items::Effects::Result ApplyEventModelEffect(BMD* b, OBJECT* o, i return Result::None; } +// Items have the effect of their model entry; the event models of level +// variants have theirs in ApplyEventModelEffect. +Render::Items::Effects::Result ApplyPartObjectEffect(BMD* b, OBJECT* o, int Type, float Alpha, int& Level, + int ItemLevel) +{ + const Render::Items::Effects::Result effect = Render::Items::Effects::Apply(b, o, Type, Alpha, Level); + return effect != Render::Items::Effects::Result::None ? effect + : ApplyEventModelEffect(b, o, Type, Level, ItemLevel); +} + extern float g_Luminosity; void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int ItemLevel, int ExcellentFlags, @@ -6791,14 +6801,7 @@ void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int Level = std::min(Level, g_pOption->GetRenderLevel() * 2 + 5); } - // Items have the effect of their model entry; the event models of level - // variants have theirs in ApplyEventModelEffect. - 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); - } - if (effect == Render::Items::Effects::Result::Drawn) + if (ApplyPartObjectEffect(b, o, Type, Alpha, Level, ItemLevel) == Render::Items::Effects::Result::Drawn) { return; } @@ -6852,11 +6855,25 @@ void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int } else if (Level < 3) { - // Some items shine below +3 (the effect of their model entry). - if (!Render::Items::Effects::RenderBelowPlus3(b, o, Type, Alpha, RenderType, Light)) + // Some items shine below +3; their render style says how. + const Render::Items::Styles::ShineBelowPlus3* shine = Render::Items::Styles::FindShineBelowPlus3(Type); + if (shine == nullptr) { VectorCopy(Light, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); + } + else if (shine->light) + { + const float factor = *shine->light; + Vector(Light[0] * factor, Light[1] * factor, Light[2] * factor, b->BodyLight); + } + RenderPartObjectBody(b, o, Type, Alpha, RenderType); + if (shine != nullptr) + { + for (const Render::Items::Styles::ShineBelowPlus3::Pass& pass : shine->passes) + { + RenderPartObjectBodyColor2(b, o, Type, pass.alpha, pass.renderType | (RenderType & RENDER_EXTRA), + pass.bright); + } } } else if (Level < 5) diff --git a/src/source/Engine/Object/ZzzObject.h b/src/source/Engine/Object/ZzzObject.h index d394a7f9e..c1fb925fb 100644 --- a/src/source/Engine/Object/ZzzObject.h +++ b/src/source/Engine/Object/ZzzObject.h @@ -1,5 +1,6 @@ #pragma once #include "UI/NewUI/Inventory/NewUIItemMng.h" +#include "Render/Items/ItemEffects.h" extern OBJECT_BLOCK ObjectBlock[256]; extern OBJECT Mounts[]; @@ -64,6 +65,11 @@ void RenderPartObjectBodyColor2(BMD* b, OBJECT* o, int Type, float Alpha, int Re int Texture = -1); void RenderPartObjectBody(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType); +// Runs the effect of the model before it is drawn: the one of its item's +// model entry, or for the event models of level variants theirs. It may +// change the level the model glows like. +Render::Items::Effects::Result ApplyPartObjectEffect(BMD* b, OBJECT* o, int Type, float Alpha, int& Level, + int ItemLevel); void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha = 0.f, int Level = 0, int ExcellentFlags = 0, int ancientDiscriminator = 0, int Select = 0, int RenderType = RENDER_TEXTURE); void RenderPartObject(OBJECT* o, int Type, void* p, vec3_t Light, float Alpha = 0.f, int Level = 0, diff --git a/src/source/Render/Items/ItemEffects.cpp b/src/source/Render/Items/ItemEffects.cpp index cbe43c742..77f03433f 100644 --- a/src/source/Render/Items/ItemEffects.cpp +++ b/src/source/Render/Items/ItemEffects.cpp @@ -1,12 +1,11 @@ #include "stdafx.h" #include "ItemEffects.h" -#include "ItemModelLookup.h" +#include "ItemModelTable.h" #include "Core/Globals/_enum.h" #include "Engine/Object/w_ObjectInfo.h" #include "Engine/Object/ZzzCharacter.h" -#include "Engine/Object/ZzzObject.h" #include "Render/Effects/ZzzEffect.h" #include "Render/Models/ZzzBMD.h" @@ -20,14 +19,11 @@ namespace // An effect runs before the model is drawn, and may change the level the // model glows like. Drawn: it drew the model itself, nothing more is drawn. using EffectFunction = Result (*)(BMD* b, OBJECT* o, int Type, float Alpha, int& Level); -// Draws the model below +3 instead of the plain drawing. -using ShineFunction = void (*)(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType, float* Light); struct ItemEffect { const char* name; - EffectFunction apply = nullptr; - ShineFunction renderBelowPlus3 = nullptr; + EffectFunction apply; }; // ------------------------------------------------ the effects @@ -47,18 +43,15 @@ Result ApplyMeshesPerLevel(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) 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) - { - } - else if (Level == 2) + switch (Level) { + case 2: b->RenderMesh(1, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); Vector(0.75f, 0.65f, 0.5f, b->BodyLight); b->RenderMesh(1, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, BITMAP_CHROME); - } - else if (Level == 3) - { + break; + case 3: b->RenderMesh(1, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); b->RenderMesh(2, RENDER_TEXTURE, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV); Vector(0.75f, 0.65f, 0.5f, b->BodyLight); @@ -66,6 +59,7 @@ Result ApplyMeshesPerLevel(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) o->BlendMeshTexCoordV, BITMAP_CHROME); b->RenderMesh(2, RENDER_BRIGHT | RENDER_CHROME, o->Alpha, -1, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, BITMAP_CHROME); + break; } b->StreamMesh = -1; return Result::Drawn; @@ -173,10 +167,10 @@ Result ApplyBloodBone(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) 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; + b->StreamMesh = 0; b->RenderBody(RENDER_TEXTURE | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME); - Models[o->Type].StreamMesh = -1; + b->StreamMesh = -1; return Result::Drawn; } @@ -556,31 +550,6 @@ Result ApplyWingOfDimension(BMD* b, OBJECT* o, int Type, float Alpha, int& Level return Result::Applied; } -// ------------------------------------------------ the shine below +3 - -void RenderCursedCastleWater(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType, float* Light) -{ - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - RenderPartObjectBodyColor2(b, o, Type, 0.5f, RENDER_TEXTURE | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 0.5f); - 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) -{ - VectorCopy(Light, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); - RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); -} - -void RenderSealShine(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType, float* Light) -{ - Vector(Light[0] * 0.9f, Light[1] * 0.9f, Light[2] * 0.9f, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); - RenderPartObjectBodyColor2(b, o, Type, 1.5f, RENDER_CHROME2 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.5f); - RenderPartObjectBodyColor2(b, o, Type, 1.f, RENDER_CHROME4 | RENDER_BRIGHT | (RenderType & RENDER_EXTRA), 1.f); -} - // ------------------------------------------------ the names const ItemEffect ItemEffects[] = { @@ -611,9 +580,6 @@ const ItemEffect ItemEffects[] = { {"wingOfIllusion", ApplyWingOfIllusion}, {"wingOfRuin", ApplyWingOfRuin}, {"wingOfDimension", ApplyWingOfDimension}, - {"cursedCastleWater", nullptr, RenderCursedCastleWater}, - {"harmonyShine", nullptr, RenderHarmonyShine}, - {"sealShine", nullptr, RenderSealShine}, }; const ItemEffect* FindEffect(std::string_view name) @@ -641,21 +607,6 @@ bool Exists(std::string_view name) Result Apply(BMD* b, OBJECT* o, int modelType, float alpha, int& level) { const ItemEffect* effect = g_itemEffects.Find(modelType); - if (effect == nullptr || effect->apply == nullptr) - { - return Result::None; - } - return effect->apply(b, o, modelType, alpha, level); -} - -bool RenderBelowPlus3(BMD* b, OBJECT* o, int modelType, float alpha, int renderType, float* light) -{ - const ItemEffect* effect = g_itemEffects.Find(modelType); - if (effect == nullptr || effect->renderBelowPlus3 == nullptr) - { - return false; - } - effect->renderBelowPlus3(b, o, modelType, alpha, renderType, light); - return true; + return effect != nullptr ? effect->apply(b, o, modelType, alpha, level) : Result::None; } } // namespace Render::Items::Effects diff --git a/src/source/Render/Items/ItemEffects.h b/src/source/Render/Items/ItemEffects.h index 5999dbe27..a0e564ad8 100644 --- a/src/source/Render/Items/ItemEffects.h +++ b/src/source/Render/Items/ItemEffects.h @@ -5,9 +5,9 @@ class BMD; class OBJECT; -// What item models do besides being drawn: sprites, particles and lightning -// on their bones, a pulsing glow mesh, a mesh hidden by level, their own -// drawing instead of the plain one, the shine of some items below +3. Each +// What item models do before they are drawn: sprites, particles and +// lightning on their bones, a pulsing glow mesh, a mesh hidden by level, the +// level they glow like, or their own drawing instead of the usual one. Each // effect is code with a name; the model entry of an item names its effect // (Data/Items/Models, "effect"). Items with the same effect share it. namespace Render::Items::Effects @@ -30,8 +30,4 @@ enum class Result // taken from the item model database after each build of it). It may change // the level the model glows like. Result Apply(BMD* b, OBJECT* o, int modelType, float alpha, int& level); - -// Draws the model below +3 for the effects that shine then, instead of the -// plain drawing. False for the others. -bool RenderBelowPlus3(BMD* b, OBJECT* o, int modelType, float alpha, int renderType, float* light); } // namespace Render::Items::Effects diff --git a/src/source/Render/Items/ItemModelLookup.h b/src/source/Render/Items/ItemModelLookup.h index 92c3a351d..49ce15a61 100644 --- a/src/source/Render/Items/ItemModelLookup.h +++ b/src/source/Render/Items/ItemModelLookup.h @@ -3,9 +3,6 @@ #include "Core/Globals/_enum.h" #include "Data/GameData/ItemData/ItemModelDatabase.h" -#include -#include - namespace Render::Items { // The item model entry of a model slot (MODEL_ITEM + item type), or nullptr @@ -29,47 +26,4 @@ inline int GetItemTypeOfModel(int modelType) const int itemType = modelType - MODEL_ITEM; return Data::Items::IsValidItemType(itemType) ? itemType : -1; } - -// 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 class ItemModelTable -{ -public: - using Take = T (*)(const Data::Items::ItemModelDefinition& model); - - explicit ItemModelTable(Take take) : m_take(take) {} - - // The value of the item a model slot is drawn like (GetItemTypeOfModel); - // T{} for models that are not items and for slots without a model. - T Find(int modelType) - { - const int itemType = GetItemTypeOfModel(modelType); - return itemType >= 0 ? Values()[itemType] : T{}; - } - -private: - const std::vector& Values() - { - const int databaseVersion = g_ItemModelDatabase.GetVersion(); - if (m_databaseVersion != databaseVersion) - { - m_databaseVersion = databaseVersion; - const std::span models = g_ItemModelDatabase.GetAllSlots(); - m_values.assign(models.size(), T{}); - for (size_t itemType = 0; itemType < models.size(); ++itemType) - { - if (models[itemType].Exists()) - { - m_values[itemType] = m_take(models[itemType]); - } - } - } - return m_values; - } - - Take m_take; - int m_databaseVersion = -1; - std::vector m_values; -}; } // namespace Render::Items diff --git a/src/source/Render/Items/ItemModelTable.h b/src/source/Render/Items/ItemModelTable.h new file mode 100644 index 000000000..d2cccd398 --- /dev/null +++ b/src/source/Render/Items/ItemModelTable.h @@ -0,0 +1,54 @@ +#pragma once + +#include "ItemModelLookup.h" + +#include "Data/GameData/ItemData/ItemModelDatabase.h" + +#include +#include + +namespace Render::Items +{ +// 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 class ItemModelTable +{ +public: + using Take = T (*)(const Data::Items::ItemModelDefinition& model); + + explicit ItemModelTable(Take take) : m_take(take) {} + + // The value of the item a model slot is drawn like (GetItemTypeOfModel); + // T{} for models that are not items and for slots without a model. + T Find(int modelType) + { + const int itemType = GetItemTypeOfModel(modelType); + return itemType >= 0 ? Values()[itemType] : T{}; + } + +private: + const std::vector& Values() + { + const int databaseVersion = g_ItemModelDatabase.GetVersion(); + if (m_databaseVersion != databaseVersion) + { + m_databaseVersion = databaseVersion; + const std::span models = g_ItemModelDatabase.GetAllSlots(); + m_values.assign(models.size(), T{}); + for (size_t itemType = 0; itemType < models.size(); ++itemType) + { + if (models[itemType].Exists()) + { + m_values[itemType] = m_take(models[itemType]); + } + } + } + return m_values; + } + + Take m_take; + int m_databaseVersion = -1; + std::vector m_values; +}; +} // namespace Render::Items diff --git a/src/source/Render/Items/ItemRenderStyles.cpp b/src/source/Render/Items/ItemRenderStyles.cpp index 49aa0b616..1d3782087 100644 --- a/src/source/Render/Items/ItemRenderStyles.cpp +++ b/src/source/Render/Items/ItemRenderStyles.cpp @@ -1,7 +1,7 @@ #include "stdafx.h" #include "ItemRenderStyles.h" -#include "ItemModelLookup.h" +#include "ItemModelTable.h" #include "Core/Globals/_enum.h" #include "Engine/Object/w_ObjectInfo.h" @@ -9,9 +9,7 @@ #include "Render/Models/ZzzBMD.h" #include -#include #include -#include namespace Render::Items::Styles { @@ -34,10 +32,17 @@ struct RenderStyle { } + // Styles that shine below +3. + constexpr RenderStyle(const char* styleName, StyleFunction recipe, const ShineBelowPlus3& shineBelowPlus3) + : name(styleName), render(recipe), shine(&shineBelowPlus3) + { + } + // Styles that share a recipe and only differ in its texture; they cannot // leave the texture out. - constexpr RenderStyle(const char* styleName, TexturedStyleFunction recipe, int recipeTexture) - : name(styleName), texturedRender(recipe), texture(recipeTexture) + constexpr RenderStyle(const char* styleName, TexturedStyleFunction recipe, int recipeTexture, + const ShineBelowPlus3* shineBelowPlus3 = nullptr) + : name(styleName), texturedRender(recipe), texture(recipeTexture), shine(shineBelowPlus3) { } @@ -52,10 +57,27 @@ struct RenderStyle TexturedStyleFunction texturedRender = nullptr; int texture = 0; GlowFunction glow = nullptr; + const ShineBelowPlus3* shine = nullptr; }; +// ------------------------------------------------ the shine below +3 + +constexpr ShineBelowPlus3 HarmonyShine{ + 1.f, {{{1.5f, RENDER_CHROME2 | RENDER_BRIGHT, 1.5f}, {1.f, RENDER_CHROME4 | RENDER_BRIGHT, 1.f}}}}; +constexpr ShineBelowPlus3 SealShine{ + 0.9f, {{{1.5f, RENDER_CHROME2 | RENDER_BRIGHT, 1.5f}, {1.f, RENDER_CHROME4 | RENDER_BRIGHT, 1.f}}}}; +// Drawn with the light the model has. +constexpr ShineBelowPlus3 CursedCastleWaterShine{ + std::nullopt, {{{0.5f, RENDER_TEXTURE | RENDER_BRIGHT, 0.5f}, {1.f, RENDER_CHROME4 | RENDER_BRIGHT, 1.f}}}}; + // ------------------------------------------------ the recipes +// For styles that only shine below +3: the drawing code draws the model. +bool RenderPlainly(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType) +{ + return false; +} + bool RenderStormCrow(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType) { b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, @@ -1719,9 +1741,10 @@ const RenderStyle RenderStyles[] = { {"guardianShield", RenderGuardianShield}, {"crossShield", RenderCrossShield}, {"oldScroll", RenderOldScroll}, - {"illusionSorcererCovenant", RenderIllusionSorcererCovenant}, + {"illusionSorcererCovenant", RenderIllusionSorcererCovenant, HarmonyShine}, {"scrollOfBlood", RenderScrollOfBlood}, - {"cursedCastleWater", RenderCursedCastleWater}, + {"cursedCastleWater", RenderCursedCastleWater, CursedCastleWaterShine}, + {"harmonyShine", RenderPlainly, HarmonyShine}, {"condorFlame", RenderCondorFlame}, {"condorFeather", RenderCondorFeather}, {"deathBeamKnightFlame", RenderThirdClassQuestItem, BITMAP_ITEM_EFFECT_DBSTONE_R}, @@ -1736,9 +1759,9 @@ const RenderStyle RenderStyles[] = { {"rareItemTicket4", RenderRareItemTicket, BITMAP_RAREITEM4_R}, {"rareItemTicket", RenderRareItemTicket, BITMAP_RAREITEM5_R}, {"talismanOfLuck", RenderTalismanOfLuck}, - {"sealOfAscension", RenderSeal, BITMAP_LUCKY_SEAL_EFFECT43}, - {"sealOfWealth", RenderSeal, BITMAP_LUCKY_SEAL_EFFECT44}, - {"sealOfSustenance", RenderSeal, BITMAP_LUCKY_SEAL_EFFECT45}, + {"sealOfAscension", RenderSeal, BITMAP_LUCKY_SEAL_EFFECT43, &SealShine}, + {"sealOfWealth", RenderSeal, BITMAP_LUCKY_SEAL_EFFECT44, &SealShine}, + {"sealOfSustenance", RenderSeal, BITMAP_LUCKY_SEAL_EFFECT45, &SealShine}, {"elitePotion", RenderElitePotion}, {"textured", RenderTextured}, {"rareItemTicket7", RenderRareItemTicket, BITMAP_RAREITEM7}, @@ -1853,6 +1876,12 @@ bool Render(BMD* b, OBJECT* o, int modelType, float alpha, int renderType) return style != nullptr && style->Render(b, o, modelType, alpha, renderType); } +const ShineBelowPlus3* FindShineBelowPlus3(int modelType) +{ + const RenderStyle* style = g_itemStyles.Find(modelType); + return style != nullptr ? style->shine : nullptr; +} + bool RenderGlow(BMD* b, OBJECT* o, int modelType, float alpha, int renderType, int texture) { const RenderStyle* style = g_itemStyles.Find(modelType); diff --git a/src/source/Render/Items/ItemRenderStyles.h b/src/source/Render/Items/ItemRenderStyles.h index 187ac19a5..001d0c50b 100644 --- a/src/source/Render/Items/ItemRenderStyles.h +++ b/src/source/Render/Items/ItemRenderStyles.h @@ -1,5 +1,7 @@ #pragma once +#include +#include #include class BMD; @@ -24,4 +26,25 @@ bool Render(BMD* b, OBJECT* o, int modelType, float alpha, int renderType); // Draws the glow pass of items +7 and up for the few render styles that glow // differently from their "glow" values. False for the others. bool RenderGlow(BMD* b, OBJECT* o, int modelType, float alpha, int renderType, int texture); + +// How a few render styles shine below +3: the light of the item scaled, the +// model drawn, then two shine passes (RenderPartObjectBodyColor2). +struct ShineBelowPlus3 +{ + struct Pass + { + float alpha; + int renderType; + float bright; + }; + + // Scales the light of the item before the model is drawn; none: the light + // stays as it is. + std::optional light; + std::array passes; +}; + +// The shine below +3 of the render style of the item; nullptr for the others, +// which are drawn plainly below +3. +const ShineBelowPlus3* FindShineBelowPlus3(int modelType); } // namespace Render::Items::Styles diff --git a/tests/data/test_item_model_files.cpp b/tests/data/test_item_model_files.cpp index 719d4cc2d..2919bc686 100644 --- a/tests/data/test_item_model_files.cpp +++ b/tests/data/test_item_model_files.cpp @@ -13,6 +13,7 @@ #include "Data/GameData/ItemData/ItemModelJsonFormat.h" #include "Data/GameData/ItemData/ItemTextureFiles.h" #include "Data/GameData/ItemData/ItemType.h" +#include "Engine/Object/ZzzObject.h" #include "GameLogic/Social/MonkSystem.h" #include "Render/Items/ItemDisplay.h" #include "Render/Items/ItemEffects.h" @@ -27,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -745,12 +747,9 @@ TEST_CASE("Shipped item models keep the effects of the old drawing code [data][i // Items with the same code share it. CHECK(effectOf(14, 7) == "hiddenMeshByLevel"); CHECK(effectOf(13, 7) == "hiddenMeshByLevel"); - // The shine below +3. - CHECK(effectOf(13, 43) == "sealShine"); - CHECK(effectOf(13, 94) == "sealShine"); - CHECK(effectOf(14, 42) == "harmonyShine"); - CHECK(effectOf(13, 50) == "harmonyShine"); - CHECK(effectOf(14, 64) == "cursedCastleWater"); + // The shine below +3 is part of the render style. + CHECK(effectOf(13, 43).empty()); + CHECK(effectOf(14, 64).empty()); // The socket seeds and spheres and zen glow like level 0, whatever their // level (the old code set their level to 0, or drew zen plainly). @@ -801,14 +800,84 @@ TEST_CASE("Item effects run before the model is drawn [data][items]") object.Type = MODEL_PLAYER; CHECK(Effects::Apply(&model, &object, MODEL_PLAYER, 1.f, level) == Result::None); - // The shine below +3 of the seals; other items are drawn plainly. - vec3_t light = {1.f, 1.f, 1.f}; - object.Type = MODEL_ITEM + ITEM_KRIS; - CHECK_FALSE(Effects::RenderBelowPlus3(&model, &object, object.Type, 1.f, RENDER_TEXTURE, light)); - object.Type = MODEL_ITEM + MakeItemType(13, 43); - CHECK(Effects::RenderBelowPlus3(&model, &object, object.Type, 1.f, RENDER_TEXTURE, light)); - // The effects follow the database when it is built again. g_ItemModelDatabase.Build({}, Data::Effects::GlowColorList{}); CHECK(apply(MakeItemType(14, 0), level) == Result::None); } + +// The drawing below +3 of the old RenderPartObjectEffect: the light of the +// item (scaled), the model, then two shine passes (RenderPartObjectBodyColor2). +TEST_CASE("Some render styles shine below +3 like the old drawing code [data][items]") +{ + using namespace Render::Items; + using Shine = Styles::ShineBelowPlus3; + const std::vector& models = ShippedModels().models; + g_ItemModelDatabase.Build(models, Data::Effects::GlowColorList{}); + + const Shine::Pass chrome2{1.5f, RENDER_CHROME2 | RENDER_BRIGHT, 1.5f}; + const Shine::Pass chrome4{1.f, RENDER_CHROME4 | RENDER_BRIGHT, 1.f}; + const auto shineOf = [](int group, int number) + { return Styles::FindShineBelowPlus3(MODEL_ITEM + MakeItemType(group, number)); }; + const auto sameAs = + [](const Shine* shine, std::optional light, const Shine::Pass& first, const Shine::Pass& second) + { + const auto samePass = [](const Shine::Pass& a, const Shine::Pass& b) + { return a.alpha == b.alpha && a.renderType == b.renderType && a.bright == b.bright; }; + return shine != nullptr && shine->light == light && samePass(shine->passes[0], first) && + samePass(shine->passes[1], second); + }; + + // The seals: the light at 0.9. + for (const auto& [group, number] : {std::pair{13, 43}, {13, 44}, {13, 45}, {13, 93}, {13, 94}, {13, 116}}) + { + INFO("(" << group << "," << number << ")"); + CHECK(sameAs(shineOf(group, number), 0.9f, chrome2, chrome4)); + } + // The Illusion Sorcerer Covenant, the Jewel of Harmony and the Moonstone + // Pendant: the light as it is. + for (const auto& [group, number] : {std::pair{13, 50}, {14, 42}, {13, 38}}) + { + INFO("(" << group << "," << number << ")"); + CHECK(sameAs(shineOf(group, number), 1.f, chrome2, chrome4)); + } + // The water of the Cursed Castle keeps the light the model has. + CHECK(sameAs(shineOf(14, 64), std::nullopt, Shine::Pass{0.5f, RENDER_TEXTURE | RENDER_BRIGHT, 0.5f}, chrome4)); + + // Other items are drawn plainly below +3; so are the Jewel of Harmony and + // the Moonstone Pendant above it (their style only shines). + CHECK(shineOf(0, 0) == nullptr); + CHECK(shineOf(12, 37) == nullptr); + BMD model; + OBJECT object; + object.Type = MODEL_ITEM + MakeItemType(14, 42); + CHECK_FALSE(Styles::Render(&model, &object, object.Type, 1.f, RENDER_TEXTURE)); + + g_ItemModelDatabase.Build({}, Data::Effects::GlowColorList{}); +} + +TEST_CASE("The effect of a model is the one of its item, or of its event model [data][items]") +{ + using Render::Items::Effects::Result; + g_ItemModelDatabase.Build(ShippedModels().models, Data::Effects::GlowColorList{}); + + BMD model; + OBJECT object; + int level = 3; + // An item with an effect: its effect, whatever the object is. + object.Type = MODEL_EVENT + 11; + CHECK(ApplyPartObjectEffect(&model, &object, MODEL_ITEM + MakeItemType(14, 0), 1.f, level, 3) == Result::Applied); + CHECK(level == 7); + // The event models of level variants have theirs in the drawing code: they + // only run when the item has no effect. + object.Type = MODEL_EVENT + 11; + CHECK(ApplyPartObjectEffect(&model, &object, MODEL_ITEM + ITEM_KRIS, 1.f, level, 0) == Result::Drawn); + object.Type = MODEL_EVENT + 18; + object.BlendMesh = -1; + CHECK(ApplyPartObjectEffect(&model, &object, MODEL_EVENT + 18, 1.f, level, 0) == Result::Applied); + CHECK(object.BlendMesh == 1); + // Neither. + object.Type = MODEL_ITEM + ITEM_KRIS; + CHECK(ApplyPartObjectEffect(&model, &object, object.Type, 1.f, level, 0) == Result::None); + + g_ItemModelDatabase.Build({}, Data::Effects::GlowColorList{}); +} From 2e118d8670523df7f97fa264e89ef433c4c22716 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Wed, 30 Sep 2026 12:23:21 +0200 Subject: [PATCH 6/9] refactor(items): name the item effects itemEffect (phase 4c3) 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. --- docs/item-data.md | 22 +++++++------- .../2026-09-25-data-driven-items-design.md | 8 ++--- ...9-29-data-driven-content-roadmap-design.md | 4 +-- src/Localization/Editor.en.resx | 4 +-- .../UI/ItemEditor/ItemEditorLooks.cpp | 6 ++-- src/MuEditor/UI/ItemEditor/ItemEditorLooks.h | 3 +- src/bin/Data/Items/Models/Group03_Spear.json | 2 +- src/bin/Data/Items/Models/Group05_Staff.json | 2 +- src/bin/Data/Items/Models/Group07_Helm.json | 4 +-- src/bin/Data/Items/Models/Group09_Pants.json | 2 +- src/bin/Data/Items/Models/Group10_Gloves.json | 2 +- src/bin/Data/Items/Models/Group11_Boots.json | 2 +- src/bin/Data/Items/Models/Group12_Wing.json | 28 ++++++++--------- src/bin/Data/Items/Models/Group13_Helper.json | 12 ++++---- src/bin/Data/Items/Models/Group14_Potion.json | 30 +++++++++---------- .../DataHandler/ItemData/ItemModelLoader.cpp | 2 +- .../DataHandler/ItemData/ItemModelLoader.h | 6 ++-- .../DataHandler/ItemData/ItemModelProblem.cpp | 4 +-- .../DataHandler/ItemData/ItemModelProblem.h | 8 ++--- .../GameData/ItemData/ItemModelDefinition.h | 2 +- .../GameData/ItemData/ItemModelJsonFormat.cpp | 14 ++++----- src/source/Engine/Object/ZzzObject.cpp | 18 +++++------ src/source/Engine/Object/ZzzObject.h | 6 ++-- src/source/Engine/Object/ZzzOpenData.cpp | 2 +- src/source/Render/Items/ItemEffects.cpp | 7 +++-- src/source/Render/Items/ItemEffects.h | 8 ++--- tests/data/test_item_model_files.cpp | 24 +++++++-------- tests/data/test_item_model_json.cpp | 20 ++++++------- tests/data/test_item_model_problems.cpp | 6 ++-- 29 files changed, 130 insertions(+), 128 deletions(-) diff --git a/docs/item-data.md b/docs/item-data.md index f85bbeaa5..03c52a1d0 100644 --- a/docs/item-data.md +++ b/docs/item-data.md @@ -246,7 +246,7 @@ item files hold what client and server share. | `ground` | How the item lies on the ground, see below. Optional. | | `glow` | How the item glows, see below. Optional. | | `renderStyle` | The look of the model when it is more than a plain textured model, see below. Optional. | -| `effect` | What the model does before it is drawn (sprites and particles on its bones, a pulsing glow mesh, ...), see below. Optional. | +| `itemEffect` | What the model does before it is drawn (sprites and particles on its bones, a pulsing glow mesh, ...), see below. Optional. | | `cloth` | `true` for capes that are worn as cloth: when one is put on or taken off, the character's cloth is deleted, so the next cape builds its own. Optional. The flag does not make a cape cloth; which capes are drawn as cloth, and how, is still decided in code. | `inventory` and `ground` hold these values; a missing value has the @@ -346,15 +346,15 @@ and `sealOfSustenance` (the seals, light at 0.9), `illusionSorcererCovenant` and `harmonyShine` (the Jewel of Harmony and the Moonstone Pendant, which are drawn plainly otherwise), and `cursedCastleWater`. -`effect` names what the model does before it is drawn: +`itemEffect` names what the model does before it is drawn: ```json -{ "number": 37, "file": "Data/Item/wing09.bmd", "textureFolders": ["Item"], "effect": "wingOfEternal" } +{ "number": 37, "file": "Data/Item/wing09.bmd", "textureFolders": ["Item"], "itemEffect": "wingOfEternal" } ``` -Each effect is code with a name; the names are listed at the end of -`src/source/Render/Items/ItemEffects.cpp`. An effect runs before the model -is drawn and can: +Each item effect is code with a name; the names are listed at the end of +`src/source/Render/Items/ItemEffects.cpp`. An item effect runs every frame +before the model is drawn and can: - place sprites, particles and lightning on bones of the model, so they follow its animation (the Devil's Key and Invitation, Rena, the wings of @@ -368,8 +368,8 @@ is drawn and can: scrolls, `fruits`, `spirit`, `bloodBone`, `invisibilityCloak`, `firecracker`, `gmGift`, `meshesPerLevel`). -Items with the same effect share it. The socket seeds and spheres and zen -have no effect; they glow like level 0 (`"glow": {"level": 0}`), whatever +Items with the same item effect share it. The socket seeds and spheres and +zen have no item effect; they glow like level 0 (`"glow": {"level": 0}`), whatever their level. The effects of the event models that level variants are drawn with stay in @@ -419,7 +419,7 @@ The problems are: | `noneBlendMeshes` has a mesh number the model does not have. | warning | | A `glow` value names a mesh the model does not have; that glow is not drawn (a hidden mesh: the glow is on all meshes). | warning | | `renderStyle` names a style that does not exist; the model is drawn plainly. | warning | -| `effect` names an effect that does not exist; the model is drawn without it. | warning | +| `itemEffect` names an item effect that does not exist; the model is drawn without it. | warning | The model names textures as `.jpg`/`.tga`; the game reads the encrypted copies with the same name, `.OZJ`/`.OZT`. Meshes whose texture name starts @@ -458,8 +458,8 @@ To keep your changes, copy the changed files from The **Looks** section above the item table shows the model data of the selected item (`Data/Items/Models`), read only: its model file, its glow -values, its render style and its effect. A render style or an effect -opens to the list of all items that use it; clicking one selects it in +values, its render style and its item effect. A render style or an item +effect opens to the list of all items that use it; clicking one selects it in the table (the search is cleared when it hides that item). Items without item data are listed too; they are not in the table, but selecting one shows its looks. Changing the looks is done in the model files for now. diff --git a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md index ea013549c..2ff27741c 100644 --- a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md +++ b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md @@ -56,7 +56,7 @@ share with items) is planned in the | D19 | Editor sync | Every item editor change goes into the database right away (phase 2), so the editor and the database never differ. Moving the editor fully onto the database stays in phase 6. | | D22 | Model data files | Model and display data (model file, textures, inventory and ground display, cloth, effects) lives in separate files, `Data/Items/Models/GroupNN_*.json`, one per item group, items by `number`. It is client-only: the item files keep what client and server share, and only those take part in the OpenMU exchange. Separate files also keep the item files small and let the model editor and the stats editor change different files. | | D23 | Model slots | Two steps. **Own model slots (phase 4a–4c):** every item keeps its own model slot, `MODEL_ITEM + item type`, as today; the data only says which file is loaded into it, so the ~100 places that compute `+ MODEL_ITEM` / `- MODEL_ITEM` stay unchanged. **Shared models (phase 4d, right after them):** models become entries of their own, and items reference them by name, so several items can share one model (loaded once). The code then asks the item database for an item's model slot, and objects keep their item type instead of computing it back from the model. Phase 12 (level variants as items of their own) then shares models without loading them twice. | -| D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and an effect (the item branches of `RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. The named looks are code for now; D25 moves what they are made of into data. | +| D24 | Render effects | The effect code stays code, but which item uses which effect moves into the model data: the glow as values (color names from a glow color list, `Data/Effects/GlowColors.json`, which has the colors of the old `PartObjectColor*` palettes; the meshes it is drawn on; the level it glows like), a render style (`RenderPartObjectBody` recipes; identical recipes share one name) and an item effect (the item branches of `RenderPartObjectEffect`). Effects that depend on the item level (+7 glow, excellent, ancient) stay generic code, and the entries for monsters and NPCs stay in code. The named looks are code for now; D25 moves what they are made of into data. | | D25 | Looks in data | Items keep naming their looks; what a named look is made of moves from code into data (phase 13). Skills, monsters and NPCs use the same look format, so the decision lives in the [roadmap](2026-09-29-data-driven-content-roadmap-design.md). | | D26 | Shared definitions | Defined once with a name and referenced by it; editors show where a definition is used and warn with that list before a shared definition changes. The decision lives in the [roadmap](2026-09-29-data-driven-content-roadmap-design.md). | | D21 | New item groups | *To discuss again when we reach phase 12.* Items may move into new groups (e.g. 16 = jewels, 17 = orbs) for the new client, while original Season 6 clients keep the old ids. Moved items keep their original id as a legacy id; OpenMU's Season 6 item serializer sends the legacy id, a serializer for the new client sends the new id. Planned after phases 3, 4 and OpenMU PR A, when little code depends on group numbers any more. | @@ -532,7 +532,7 @@ server with original clients (after phases 6 and B). "inventory": { "offset": [-0.02, 0.03], "rotation": [180, 270, 15], "scale": 0.0039 }, "ground": { "rotation": [60, 0, -45], "scale": 1.0 }, "glow": { "color": "gold", "meshes": [0] }, - "renderStyle": "chromeMesh0", "effect": "flameSparks" } + "renderStyle": "chromeMesh0", "itemEffect": "flameSparks" } ``` Every item keeps its model slot `MODEL_ITEM + item type` (D23, own model slots). Three @@ -590,8 +590,8 @@ server with original clients (after phases 6 and B). loaded their models); their recipe (textured, then `RENDER_BRIGHT | RENDER_CHROME2`) comes back as a style with their model entries. - - **4c3 Item effects:** `effect` names what the model does before it - is drawn, from the type chain of `RenderPartObjectEffect`: 29 item + - **4c3 Item effects:** `itemEffect` names what the model does before + it is drawn every frame, from the type chain of `RenderPartObjectEffect`: 29 item branches for 81 items; 28 of them become 27 effects for 42 items in `Render/Items/ItemEffects.cpp` (moved unchanged; the Siege Potion and the Contract share one). An effect places sprites, particles and diff --git a/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md b/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md index b5cf0aa3d..6da5f4d85 100644 --- a/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md +++ b/docs/superpowers/specs/2026-09-29-data-driven-content-roadmap-design.md @@ -39,7 +39,7 @@ so the references there stay valid. | # | Topic | Decision | |---|---|---| -| D25 | Looks in data | Items, skills, monsters and NPCs reference their looks by name (for items: the render style and the effect; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/`: a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; data that names looks stays valid when their definitions move. One look format and one look editor for all areas: a look made for an item can be used by a monster. Effect and particle types are referenced by the names of the effect catalogue (FX1) and become data themselves later (FX2). | +| D25 | Looks in data | Items, skills, monsters and NPCs reference their looks by name (for items: the render style and the item effect; the glow colors already are names in data). What a named look is made of moves from code into data files in `Data/Effects/`: a list of building blocks, namely draw passes (mesh or body, flags, texture, color, alpha, texture scrolling), things placed on bones (sprites, particles, lightning between two bones, effects), animated object values (glow mesh brightness, hidden mesh, texture scrolling), timing (pulses, random chances per frame) and conditions (item level, doppelganger, ...). The drawing code runs these lists; data that names looks stays valid when their definitions move. One look format and one look editor for all areas: a look made for an item can be used by a monster. Effect and particle types are referenced by the names of the effect catalogue (FX1) and become data themselves later (FX2). | | D26 | Shared definitions | Anything that several things use is defined once, with a name, in a data file, and its users reference that name: glow colors, looks, effect types, skills. Editors show where a definition is used (items, skills, monsters, NPCs, other effects). Saving a change to a definition that others use first shows the list of those users; a copy makes a variant for one user; renaming updates all references; a definition that is still used cannot be deleted (the list shows why). Unknown names are reported when loading. Names are resolved when loading, so drawing and game logic never look names up. Every move from code into data is compared with the old code (recorder) before the old code goes. | ## Bones @@ -68,7 +68,7 @@ clicking them. | Area | Name | Design document | Depends on | What moves into data | Editor | |---|---|---|---|---|---| | Items | Items | [items design](2026-09-25-data-driven-items-design.md), phases 0–14 | – | Item data, rules and categories, models, glow, render styles and effects (as names). | The item tools of section 9 there. | -| FX1 | Effect catalogue | not yet | items 4c | Every effect, particle, lightning and sprite type gets a name and its creation values (the `CreateParams` of the registry) in `Data/Effects/`. Behavior stays code. Items, skills and monsters then name effects instead of using type numbers. | Effect browser: each effect with a preview, its values, and where it is used. | +| FX1 | Effect catalogue | not yet | items 4c | Every effect, particle, lightning and sprite type gets a name and its creation values (the `CreateParams` of the registry) in `Data/Effects/`: the effect types (`effectTypes` in the data). Behavior stays code. Items, skills and monsters then name effect types instead of using type numbers; an item effect (`itemEffect`, what an item does every frame before it is drawn) creates instances of effect types. | Effect browser: each effect with a preview, its values, and where it is used. | | Items 13 | Looks in data (items) | items design, phase 13 | FX1, items 6 | What the item looks are made of (D25). | Look editor. | | SK1 | Skills as data | not yet | items 2 (the same format rules) | `Skill.bmd` into JSON, like the items in their phase 2: names, requirements, rules; later synced with OpenMU like the items. | Focused skill editors, like the item tools; "used by" (classes, items that give a skill, monsters). | | SK2 | Skill looks | not yet | SK1, FX1, items 13 | How a skill looks when cast, flying and hitting (effects, particles, sounds, character animations), as looks (D25), mostly on the caster's bones. | Look editor, with a preview of the skill cast by a test character. | diff --git a/src/Localization/Editor.en.resx b/src/Localization/Editor.en.resx index ce5346046..8a1a90d6e 100644 --- a/src/Localization/Editor.en.resx +++ b/src/Localization/Editor.en.resx @@ -475,8 +475,8 @@ Render style - - Effect + + Item effect used by diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp index f9a3dfc7d..2e9556256 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp @@ -39,7 +39,7 @@ struct LooksCache int databaseVersion = -1; std::string glow; Look renderStyle; - Look effect; + Look itemEffect; }; LooksCache g_cache; @@ -82,7 +82,7 @@ void Refresh(int itemType, const ItemModelDefinition& model) g_cache.databaseVersion = databaseVersion; g_cache.glow = DescribeGlow(model); g_cache.renderStyle = TakeLook(model, &ItemModelDefinition::renderStyle); - g_cache.effect = TakeLook(model, &ItemModelDefinition::effect); + g_cache.itemEffect = TakeLook(model, &ItemModelDefinition::itemEffect); } // A look with the number of items that use it; opened, the list of those @@ -138,7 +138,7 @@ int CItemEditorLooks::Render(int itemType) ImGui::Text("%s: %s", I18N::Editor::ModelFile, model->file.c_str()); ImGui::Text("%s: %s", I18N::Editor::Glow, g_cache.glow.c_str()); const int clickedStyleUser = RenderLook(I18N::Editor::RenderStyle, g_cache.renderStyle); - const int clickedEffectUser = RenderLook(I18N::Editor::Effect, g_cache.effect); + const int clickedEffectUser = RenderLook(I18N::Editor::ItemEffect, g_cache.itemEffect); return clickedStyleUser >= 0 ? clickedStyleUser : clickedEffectUser; } diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h index 629889cc1..d7c68f95d 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h @@ -3,7 +3,8 @@ #ifdef _EDITOR // A read-only view of the looks of the selected item (Data/Items/Models): its -// model file, glow, render style and effect, and the items that share a look. +// model file, glow, render style and item effect, and the items that share a +// look. class CItemEditorLooks { public: diff --git a/src/bin/Data/Items/Models/Group03_Spear.json b/src/bin/Data/Items/Models/Group03_Spear.json index cd5368d14..8e5e16c10 100644 --- a/src/bin/Data/Items/Models/Group03_Spear.json +++ b/src/bin/Data/Items/Models/Group03_Spear.json @@ -136,7 +136,7 @@ "glow": { "color": "red" }, - "effect": "billOfBalrog" + "itemEffect": "billOfBalrog" }, { "number": 10, diff --git a/src/bin/Data/Items/Models/Group05_Staff.json b/src/bin/Data/Items/Models/Group05_Staff.json index 7415294d8..4b58f3ac3 100644 --- a/src/bin/Data/Items/Models/Group05_Staff.json +++ b/src/bin/Data/Items/Models/Group05_Staff.json @@ -172,7 +172,7 @@ "color": "bronze" }, "renderStyle": "staffOfKundun", - "effect": "staffOfKundun" + "itemEffect": "staffOfKundun" }, { "number": 12, diff --git a/src/bin/Data/Items/Models/Group07_Helm.json b/src/bin/Data/Items/Models/Group07_Helm.json index b80de398a..8cd2092dc 100644 --- a/src/bin/Data/Items/Models/Group07_Helm.json +++ b/src/bin/Data/Items/Models/Group07_Helm.json @@ -292,7 +292,7 @@ "glow": { "color": "steelBlue" }, - "effect": "divineSet" + "itemEffect": "divineSet" }, { "number": 21, @@ -342,7 +342,7 @@ "glow": { "color": "copper" }, - "effect": "redSpirit" + "itemEffect": "redSpirit" }, { "number": 25, diff --git a/src/bin/Data/Items/Models/Group09_Pants.json b/src/bin/Data/Items/Models/Group09_Pants.json index a15617f3c..8857424f4 100644 --- a/src/bin/Data/Items/Models/Group09_Pants.json +++ b/src/bin/Data/Items/Models/Group09_Pants.json @@ -422,7 +422,7 @@ "glow": { "color": "copper" }, - "effect": "redSpirit" + "itemEffect": "redSpirit" }, { "number": 25, diff --git a/src/bin/Data/Items/Models/Group10_Gloves.json b/src/bin/Data/Items/Models/Group10_Gloves.json index 86efec582..ee5bf5ca5 100644 --- a/src/bin/Data/Items/Models/Group10_Gloves.json +++ b/src/bin/Data/Items/Models/Group10_Gloves.json @@ -330,7 +330,7 @@ "glow": { "color": "steelBlue" }, - "effect": "divineSet" + "itemEffect": "divineSet" }, { "number": 20, diff --git a/src/bin/Data/Items/Models/Group11_Boots.json b/src/bin/Data/Items/Models/Group11_Boots.json index e659fa582..f4dcdf051 100644 --- a/src/bin/Data/Items/Models/Group11_Boots.json +++ b/src/bin/Data/Items/Models/Group11_Boots.json @@ -230,7 +230,7 @@ "glow": { "color": "steelBlue" }, - "effect": "divineSet" + "itemEffect": "divineSet" }, { "number": 20, diff --git a/src/bin/Data/Items/Models/Group12_Wing.json b/src/bin/Data/Items/Models/Group12_Wing.json index bf5ca26e9..40be7bf27 100644 --- a/src/bin/Data/Items/Models/Group12_Wing.json +++ b/src/bin/Data/Items/Models/Group12_Wing.json @@ -78,7 +78,7 @@ "level": 0, "excellent": false }, - "effect": "wingsOfSoul" + "itemEffect": "wingsOfSoul" }, { "number": 5, @@ -95,7 +95,7 @@ "level": 0, "excellent": false }, - "effect": "wingsOfDragon" + "itemEffect": "wingsOfDragon" }, { "number": 6, @@ -113,7 +113,7 @@ "excellent": false }, "renderStyle": "wingsOfDarkness", - "effect": "wingsOfDarkness" + "itemEffect": "wingsOfDarkness" }, { "number": 7, @@ -323,7 +323,7 @@ "ground": { "rotation": [270, 0, 45] }, - "effect": "darkLordScroll" + "itemEffect": "darkLordScroll" }, { "number": 22, @@ -335,7 +335,7 @@ "ground": { "rotation": [270, 0, 45] }, - "effect": "darkLordScroll" + "itemEffect": "darkLordScroll" }, { "number": 23, @@ -347,7 +347,7 @@ "ground": { "rotation": [270, 0, 45] }, - "effect": "darkLordScroll" + "itemEffect": "darkLordScroll" }, { "number": 24, @@ -359,7 +359,7 @@ "ground": { "rotation": [270, 0, 45] }, - "effect": "darkLordScroll" + "itemEffect": "darkLordScroll" }, { "number": 30, @@ -440,7 +440,7 @@ "ground": { "rotation": [270, 0, 45] }, - "effect": "darkLordScroll" + "itemEffect": "darkLordScroll" }, { "number": 36, @@ -457,7 +457,7 @@ "excellent": false }, "renderStyle": "wingOfStorm", - "effect": "wingOfStorm" + "itemEffect": "wingOfStorm" }, { "number": 37, @@ -474,7 +474,7 @@ "level": 0, "excellent": false }, - "effect": "wingOfEternal" + "itemEffect": "wingOfEternal" }, { "number": 38, @@ -491,7 +491,7 @@ "level": 0, "excellent": false }, - "effect": "wingOfIllusion" + "itemEffect": "wingOfIllusion" }, { "number": 39, @@ -510,7 +510,7 @@ "excellent": false }, "renderStyle": "wingOfRuin", - "effect": "wingOfRuin" + "itemEffect": "wingOfRuin" }, { "number": 40, @@ -578,7 +578,7 @@ "excellent": false }, "renderStyle": "wingOfDimension", - "effect": "wingOfDimension" + "itemEffect": "wingOfDimension" }, { "number": 44, @@ -654,7 +654,7 @@ "ground": { "rotation": [270, 0, 45] }, - "effect": "darkLordScroll" + "itemEffect": "darkLordScroll" }, { "number": 49, diff --git a/src/bin/Data/Items/Models/Group13_Helper.json b/src/bin/Data/Items/Models/Group13_Helper.json index 1c6c56801..02f962d9b 100644 --- a/src/bin/Data/Items/Models/Group13_Helper.json +++ b/src/bin/Data/Items/Models/Group13_Helper.json @@ -67,7 +67,7 @@ "anchor": [0.5, 0.9] }, "renderStyle": "contractSummon", - "effect": "hiddenMeshByLevel" + "itemEffect": "hiddenMeshByLevel" }, { "number": 8, @@ -104,7 +104,7 @@ "anchor": [0.5, 0.8] }, "renderStyle": "lifeStone", - "effect": "hideMesh1" + "itemEffect": "hideMesh1" }, { "number": 12, @@ -144,7 +144,7 @@ "anchor": [0.6, 1], "scale": 0.003 }, - "effect": "fruits" + "itemEffect": "fruits" }, { "number": 16, @@ -171,7 +171,7 @@ "rotation": [-45, -5, 18], "scale": 0.48 }, - "effect": "bloodBone" + "itemEffect": "bloodBone" }, { "number": 18, @@ -186,7 +186,7 @@ "rotation": [165, -168, 198], "scale": 0.48 }, - "effect": "invisibilityCloak" + "itemEffect": "invisibilityCloak" }, { "number": 21, @@ -308,7 +308,7 @@ "anchor": [0.5, 0.9], "rotation": [-90, -90, 0] }, - "effect": "spirit" + "itemEffect": "spirit" }, { "number": 32, diff --git a/src/bin/Data/Items/Models/Group14_Potion.json b/src/bin/Data/Items/Models/Group14_Potion.json index d03f8d9e9..1940cb813 100644 --- a/src/bin/Data/Items/Models/Group14_Potion.json +++ b/src/bin/Data/Items/Models/Group14_Potion.json @@ -10,7 +10,7 @@ "anchor": [0.5, 0.95], "scale": 0.0035 }, - "effect": "potion" + "itemEffect": "potion" }, { "number": 1, @@ -20,7 +20,7 @@ "anchor": [0.5, 0.95], "scale": 0.0035 }, - "effect": "potion" + "itemEffect": "potion" }, { "number": 2, @@ -30,7 +30,7 @@ "anchor": [0.5, 0.95], "scale": 0.0035 }, - "effect": "potion" + "itemEffect": "potion" }, { "number": 3, @@ -40,7 +40,7 @@ "anchor": [0.5, 0.95], "scale": 0.0035 }, - "effect": "potion" + "itemEffect": "potion" }, { "number": 4, @@ -50,7 +50,7 @@ "anchor": [0.5, 0.95], "scale": 0.0035 }, - "effect": "potion" + "itemEffect": "potion" }, { "number": 5, @@ -60,7 +60,7 @@ "anchor": [0.5, 0.95], "scale": 0.0035 }, - "effect": "potion" + "itemEffect": "potion" }, { "number": 6, @@ -70,7 +70,7 @@ "anchor": [0.5, 0.95], "scale": 0.0035 }, - "effect": "potion" + "itemEffect": "potion" }, { "number": 7, @@ -80,7 +80,7 @@ "anchor": [0.5, 0.5] }, "renderStyle": "siegePotion", - "effect": "hiddenMeshByLevel" + "itemEffect": "hiddenMeshByLevel" }, { "number": 8, @@ -198,7 +198,7 @@ "glow": { "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, - "effect": "devilsEye" + "itemEffect": "devilsEye" }, { "number": 18, @@ -214,7 +214,7 @@ "glow": { "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, - "effect": "devilsKey" + "itemEffect": "devilsKey" }, { "number": 19, @@ -230,7 +230,7 @@ "glow": { "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, - "effect": "devilsInvitation" + "itemEffect": "devilsInvitation" }, { "number": 20, @@ -260,7 +260,7 @@ "ground": { "rotation": [270, 0, 90] }, - "effect": "rena" + "itemEffect": "rena" }, { "number": 22, @@ -337,7 +337,7 @@ "rotation": [270, 0, 0], "scale": 0.0035 }, - "effect": "meshesPerLevel" + "itemEffect": "meshesPerLevel" }, { "number": 28, @@ -642,7 +642,7 @@ "rotation": [0, 0, -10], "scale": 0.4 }, - "effect": "gmGift" + "itemEffect": "gmGift" }, { "number": 53, @@ -769,7 +769,7 @@ "rotation": [70, 0, -45], "scale": 1.5 }, - "effect": "firecracker" + "itemEffect": "firecracker" }, { "number": 64, diff --git a/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp b/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp index a1c1fe03c..98220e35f 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp +++ b/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp @@ -135,7 +135,7 @@ void OpenModel(int itemType, const ItemModelDefinition& model, const LookNames& MarkNoneBlendMeshes(itemType, model); CheckGlowMeshes(itemType, model); CheckLookName(model, model.renderStyle, lookNames.renderStyle, ItemModelProblemType::RenderStyleUnknown); - CheckLookName(model, model.effect, lookNames.effect, ItemModelProblemType::EffectUnknown); + CheckLookName(model, model.itemEffect, lookNames.itemEffect, ItemModelProblemType::ItemEffectUnknown); } ItemModelProblemType GetTextureProblemType(const TextureProblem& textureProblem) diff --git a/src/source/Data/DataHandler/ItemData/ItemModelLoader.h b/src/source/Data/DataHandler/ItemData/ItemModelLoader.h index 5a7ab8135..1cc6d37c0 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelLoader.h +++ b/src/source/Data/DataHandler/ItemData/ItemModelLoader.h @@ -18,15 +18,15 @@ inline bool NoLookExists(std::string_view) } // The looks that exist; they are drawing code (Render::Items::Styles::Exists, -// Render::Items::Effects::Exists). +// Render::Items::ItemEffects::Exists). struct LookNames { LookExists renderStyle = NoLookExists; - LookExists effect = NoLookExists; + LookExists itemEffect = NoLookExists; }; // Opens the .bmd file of every item model; `lookNames` checks the names of -// the "renderStyle" and "effect" values. +// the "renderStyle" and "itemEffect" values. void OpenModels(const LookNames& lookNames); // Loads the textures of every item model from its texture folders. diff --git a/src/source/Data/DataHandler/ItemData/ItemModelProblem.cpp b/src/source/Data/DataHandler/ItemData/ItemModelProblem.cpp index 16d394d24..41b2da9cd 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelProblem.cpp +++ b/src/source/Data/DataHandler/ItemData/ItemModelProblem.cpp @@ -64,8 +64,8 @@ std::string DescribeProblem(const ItemModelProblem& problem) std::to_string(problem.meshCount) + " meshes"; case ItemModelProblemType::RenderStyleUnknown: return "render style " + problem.name + " does not exist; " + problem.modelFile + " is drawn plainly"; - case ItemModelProblemType::EffectUnknown: - return "effect " + problem.name + " does not exist; " + problem.modelFile + " is drawn without it"; + case ItemModelProblemType::ItemEffectUnknown: + return "item effect " + problem.name + " does not exist; " + problem.modelFile + " is drawn without it"; } return {}; } diff --git a/src/source/Data/DataHandler/ItemData/ItemModelProblem.h b/src/source/Data/DataHandler/ItemData/ItemModelProblem.h index 48f3c9c7c..82e218a60 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelProblem.h +++ b/src/source/Data/DataHandler/ItemData/ItemModelProblem.h @@ -31,9 +31,9 @@ enum class ItemModelProblemType // "renderStyle" names a style that does not exist; the model is drawn // plainly. RenderStyleUnknown, - // "effect" names an effect that does not exist; the model is drawn - // without it. - EffectUnknown, + // "itemEffect" names an item effect that does not exist; the model is + // drawn without it. + ItemEffectUnknown, }; // A problem found while opening an item model or its textures. @@ -53,7 +53,7 @@ struct ItemModelProblem int meshCount = 0; // GlowMeshMissing: the glow value with the mesh, e.g. "glow.meshes". std::string field; - // RenderStyleUnknown, EffectUnknown: the name that does not exist. + // RenderStyleUnknown, ItemEffectUnknown: the name that does not exist. std::string name; // Only the problems that leave an item without its model or a texture diff --git a/src/source/Data/GameData/ItemData/ItemModelDefinition.h b/src/source/Data/GameData/ItemData/ItemModelDefinition.h index 7d00b1afb..380e66ad0 100644 --- a/src/source/Data/GameData/ItemData/ItemModelDefinition.h +++ b/src/source/Data/GameData/ItemData/ItemModelDefinition.h @@ -130,7 +130,7 @@ struct ItemModelDefinition // What the model does before it is drawn (sprites and particles on its // bones, a pulsing glow mesh, its own drawing, ...): the name of an item // effect (Render/Items/ItemEffects). Empty: none. - std::string effect; + std::string itemEffect; bool Exists() const { diff --git a/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp b/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp index f6b43d6f2..7689efd2c 100644 --- a/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp +++ b/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp @@ -24,7 +24,7 @@ constexpr const char* File = "file"; constexpr const char* TextureFolders = "textureFolders"; constexpr const char* NoneBlendMeshes = "noneBlendMeshes"; constexpr const char* RenderStyle = "renderStyle"; -constexpr const char* Effect = "effect"; +constexpr const char* ItemEffect = "itemEffect"; } // namespace Keys constexpr std::string_view ModelFileExtension = ".bmd"; @@ -87,9 +87,9 @@ OrderedJson WriteModel(const ItemModelDefinition& model) { json[Keys::RenderStyle] = model.renderStyle; } - if (!model.effect.empty()) + if (!model.itemEffect.empty()) { - json[Keys::Effect] = model.effect; + json[Keys::ItemEffect] = model.itemEffect; } return json; } @@ -214,8 +214,8 @@ void ItemModelReader::ReadNoneBlendMeshes(const OrderedJson& json, std::vectorType == MODEL_EVENT + 14 && Level == 9) { Vector(0.3f, 0.8f, 1.f, b->BodyLight); @@ -6724,14 +6724,14 @@ static Render::Items::Effects::Result ApplyEventModelEffect(BMD* b, OBJECT* o, i return Result::None; } -// Items have the effect of their model entry; the event models of level +// Items have the item effect of their model entry; the event models of level // variants have theirs in ApplyEventModelEffect. -Render::Items::Effects::Result ApplyPartObjectEffect(BMD* b, OBJECT* o, int Type, float Alpha, int& Level, - int ItemLevel) +Render::Items::ItemEffects::Result ApplyPartObjectEffect(BMD* b, OBJECT* o, int Type, float Alpha, int& Level, + int ItemLevel) { - const Render::Items::Effects::Result effect = Render::Items::Effects::Apply(b, o, Type, Alpha, Level); - return effect != Render::Items::Effects::Result::None ? effect - : ApplyEventModelEffect(b, o, Type, Level, ItemLevel); + const Render::Items::ItemEffects::Result effect = Render::Items::ItemEffects::Apply(b, o, Type, Alpha, Level); + return effect != Render::Items::ItemEffects::Result::None ? effect + : ApplyEventModelEffect(b, o, Type, Level, ItemLevel); } extern float g_Luminosity; @@ -6801,7 +6801,7 @@ void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha, int Level = std::min(Level, g_pOption->GetRenderLevel() * 2 + 5); } - if (ApplyPartObjectEffect(b, o, Type, Alpha, Level, ItemLevel) == Render::Items::Effects::Result::Drawn) + if (ApplyPartObjectEffect(b, o, Type, Alpha, Level, ItemLevel) == Render::Items::ItemEffects::Result::Drawn) { return; } diff --git a/src/source/Engine/Object/ZzzObject.h b/src/source/Engine/Object/ZzzObject.h index c1fb925fb..559316c09 100644 --- a/src/source/Engine/Object/ZzzObject.h +++ b/src/source/Engine/Object/ZzzObject.h @@ -65,11 +65,11 @@ void RenderPartObjectBodyColor2(BMD* b, OBJECT* o, int Type, float Alpha, int Re int Texture = -1); void RenderPartObjectBody(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType); -// Runs the effect of the model before it is drawn: the one of its item's +// Runs the item effect of the model before it is drawn: the one of its item's // model entry, or for the event models of level variants theirs. It may // change the level the model glows like. -Render::Items::Effects::Result ApplyPartObjectEffect(BMD* b, OBJECT* o, int Type, float Alpha, int& Level, - int ItemLevel); +Render::Items::ItemEffects::Result ApplyPartObjectEffect(BMD* b, OBJECT* o, int Type, float Alpha, int& Level, + int ItemLevel); void RenderPartObjectEffect(OBJECT* o, int Type, vec3_t Light, float Alpha = 0.f, int Level = 0, int ExcellentFlags = 0, int ancientDiscriminator = 0, int Select = 0, int RenderType = RENDER_TEXTURE); void RenderPartObject(OBJECT* o, int Type, void* p, vec3_t Light, float Alpha = 0.f, int Level = 0, diff --git a/src/source/Engine/Object/ZzzOpenData.cpp b/src/source/Engine/Object/ZzzOpenData.cpp index 0d835413c..6f86c83db 100644 --- a/src/source/Engine/Object/ZzzOpenData.cpp +++ b/src/source/Engine/Object/ZzzOpenData.cpp @@ -463,7 +463,7 @@ void OpenItems() // Without model data the game stops; the item models are not opened. if (OpenItemModelData()) { - Data::Items::ModelLoader::OpenModels({Render::Items::Styles::Exists, Render::Items::Effects::Exists}); + Data::Items::ModelLoader::OpenModels({Render::Items::Styles::Exists, Render::Items::ItemEffects::Exists}); PrepareGrandSoulPantsClothMesh(); } OpenItemEffectModels(); diff --git a/src/source/Render/Items/ItemEffects.cpp b/src/source/Render/Items/ItemEffects.cpp index 77f03433f..73e56b2ca 100644 --- a/src/source/Render/Items/ItemEffects.cpp +++ b/src/source/Render/Items/ItemEffects.cpp @@ -12,7 +12,7 @@ #include #include -namespace Render::Items::Effects +namespace Render::Items::ItemEffects { namespace { @@ -595,7 +595,8 @@ ItemModelTable g_itemEffects{[](const Data::Items::ItemModelD { // Names that are not effects are drawn without one; the // model loader reports them. - return model.effect.empty() ? nullptr : FindEffect(model.effect); + return model.itemEffect.empty() ? nullptr + : FindEffect(model.itemEffect); }}; } // namespace @@ -609,4 +610,4 @@ Result Apply(BMD* b, OBJECT* o, int modelType, float alpha, int& level) const ItemEffect* effect = g_itemEffects.Find(modelType); return effect != nullptr ? effect->apply(b, o, modelType, alpha, level) : Result::None; } -} // namespace Render::Items::Effects +} // namespace Render::Items::ItemEffects diff --git a/src/source/Render/Items/ItemEffects.h b/src/source/Render/Items/ItemEffects.h index a0e564ad8..f4ec0883f 100644 --- a/src/source/Render/Items/ItemEffects.h +++ b/src/source/Render/Items/ItemEffects.h @@ -9,8 +9,8 @@ class OBJECT; // lightning on their bones, a pulsing glow mesh, a mesh hidden by level, the // level they glow like, or their own drawing instead of the usual one. Each // effect is code with a name; the model entry of an item names its effect -// (Data/Items/Models, "effect"). Items with the same effect share it. -namespace Render::Items::Effects +// (Data/Items/Models, "itemEffect"). Items with the same effect share it. +namespace Render::Items::ItemEffects { // Whether there is an effect of this name. The model loader checks the names // of the model entries with it. @@ -26,8 +26,8 @@ enum class Result Drawn, }; -// Runs the effect of the item before its model is drawn (the effects are +// Runs the item effect before the model is drawn (the item effects are // taken from the item model database after each build of it). It may change // the level the model glows like. Result Apply(BMD* b, OBJECT* o, int modelType, float alpha, int& level); -} // namespace Render::Items::Effects +} // namespace Render::Items::ItemEffects diff --git a/tests/data/test_item_model_files.cpp b/tests/data/test_item_model_files.cpp index 2919bc686..ac1ab856e 100644 --- a/tests/data/test_item_model_files.cpp +++ b/tests/data/test_item_model_files.cpp @@ -713,29 +713,29 @@ TEST_CASE("Styles that pick something per item draw other items plainly [data][i CHECK_FALSE(Styles::Render(&model, &object, object.Type, 1.f, RENDER_TEXTURE)); } -TEST_CASE("The effects of shipped item models exist [data][items]") +TEST_CASE("The item effects of shipped item models exist [data][items]") { for (const ItemModelDefinition& model : ShippedModels().models) { - if (!model.effect.empty()) + if (!model.itemEffect.empty()) { - INFO("(" << model.group << "," << model.number << ") " << model.effect); - CHECK(Render::Items::Effects::Exists(model.effect)); + INFO("(" << model.group << "," << model.number << ") " << model.itemEffect); + CHECK(Render::Items::ItemEffects::Exists(model.itemEffect)); } } - CHECK_FALSE(Render::Items::Effects::Exists("wingOfEternl")); + CHECK_FALSE(Render::Items::ItemEffects::Exists("wingOfEternl")); } // The effects of the old drawing code (RenderPartObjectEffect), recorded per // item: spot checks. -TEST_CASE("Shipped item models keep the effects of the old drawing code [data][items]") +TEST_CASE("Shipped item models keep the item effects of the old drawing code [data][items]") { const std::vector& models = ShippedModels().models; const auto effectOf = [&](int group, int number) { const ItemModelDefinition* model = FindModel(models, group, number); REQUIRE(model != nullptr); - return model->effect; + return model->itemEffect; }; CHECK(effectOf(0, 0).empty()); @@ -766,7 +766,7 @@ TEST_CASE("Shipped item models keep the effects of the old drawing code [data][i TEST_CASE("Item effects run before the model is drawn [data][items]") { using namespace Render::Items; - using Effects::Result; + using ItemEffects::Result; g_ItemModelDatabase.Build(ShippedModels().models, Data::Effects::GlowColorList{}); // A model without meshes draws nothing, so only the values change here. @@ -775,7 +775,7 @@ TEST_CASE("Item effects run before the model is drawn [data][items]") const auto apply = [&](int itemType, int& level) { object.Type = MODEL_ITEM + itemType; - return Effects::Apply(&model, &object, object.Type, 1.f, level); + return ItemEffects::Apply(&model, &object, object.Type, 1.f, level); }; // Potions with a level glow like +7. @@ -798,7 +798,7 @@ TEST_CASE("Item effects run before the model is drawn [data][items]") // Items without an effect, and models that are not items. CHECK(apply(ITEM_KRIS, level) == Result::None); object.Type = MODEL_PLAYER; - CHECK(Effects::Apply(&model, &object, MODEL_PLAYER, 1.f, level) == Result::None); + CHECK(ItemEffects::Apply(&model, &object, MODEL_PLAYER, 1.f, level) == Result::None); // The effects follow the database when it is built again. g_ItemModelDatabase.Build({}, Data::Effects::GlowColorList{}); @@ -855,9 +855,9 @@ TEST_CASE("Some render styles shine below +3 like the old drawing code [data][it g_ItemModelDatabase.Build({}, Data::Effects::GlowColorList{}); } -TEST_CASE("The effect of a model is the one of its item, or of its event model [data][items]") +TEST_CASE("The item effect of a model is the one of its item, or of its event model [data][items]") { - using Render::Items::Effects::Result; + using Render::Items::ItemEffects::Result; g_ItemModelDatabase.Build(ShippedModels().models, Data::Effects::GlowColorList{}); BMD model; diff --git a/tests/data/test_item_model_json.cpp b/tests/data/test_item_model_json.cpp index 0b71f87c3..1338af190 100644 --- a/tests/data/test_item_model_json.cpp +++ b/tests/data/test_item_model_json.cpp @@ -53,7 +53,7 @@ ItemModelDefinition MakeDarkHorse() model.textureFolders = {"Item", "Skill"}; model.noneBlendMeshes = {1, 3}; model.renderStyle = "darkHorse"; - model.effect = "darkHorseAura"; + model.itemEffect = "darkHorseAura"; return model; } } // namespace @@ -172,21 +172,21 @@ TEST_CASE("The render style of a model is a name [data][items]") CHECK(result.models[0].renderStyle.empty()); } -TEST_CASE("The effect of a model is a name [data][items]") +TEST_CASE("The item effect of a model is a name [data][items]") { const std::string text = WriteItemModelGroupJson(13, std::vector{MakeDarkHorse()}); - CHECK(text.find(R"("effect": "darkHorseAura")") != std::string::npos); - // The effect comes after the render style. - CHECK(text.find(R"("renderStyle")") < text.find(R"("effect")")); + CHECK(text.find(R"("itemEffect": "darkHorseAura")") != std::string::npos); + // The item effect comes after the render style. + CHECK(text.find(R"("renderStyle")") < text.find(R"("itemEffect")")); - CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "effect": ["sparks"]})", "effect")); - CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "effect": "red sparks"})", "effect")); - CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "effect": ""})", "effect")); + CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "itemEffect": ["sparks"]})", "itemEffect")); + CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "itemEffect": "red sparks"})", "itemEffect")); + CHECK(HasError(R"({"number": 8, "file": "Data/Item/Ring01.bmd", "itemEffect": ""})", "itemEffect")); - // Without an effect the model is only drawn. + // Without an item effect the model is only drawn. const ReadResult result = Read(GroupFile(13, R"({"number": 8, "file": "Data/Item/Ring01.bmd"})")); REQUIRE(result.models.size() == 1); - CHECK(result.models[0].effect.empty()); + CHECK(result.models[0].itemEffect.empty()); } TEST_CASE("A model with errors is not read [data][items]") diff --git a/tests/data/test_item_model_problems.cpp b/tests/data/test_item_model_problems.cpp index 00152f527..fcfdcf6fb 100644 --- a/tests/data/test_item_model_problems.cpp +++ b/tests/data/test_item_model_problems.cpp @@ -148,15 +148,15 @@ TEST_CASE("A render style that does not exist is a warning [data][items]") "(0,33): render style stormCorw does not exist; Data/Item/Sword34.bmd is drawn plainly " + SwordModels); } -TEST_CASE("An effect that does not exist is a warning [data][items]") +TEST_CASE("An item effect that does not exist is a warning [data][items]") { ItemModelProblem problem = MakeMissingTexture(0, ""); - problem.type = ItemModelProblemType::EffectUnknown; + problem.type = ItemModelProblemType::ItemEffectUnknown; problem.name = "wingOfEternl"; CHECK_FALSE(problem.IsError()); CHECK(problem.ToLogString() == - "(0,33): effect wingOfEternl does not exist; Data/Item/Sword34.bmd is drawn without it " + SwordModels); + "(0,33): item effect wingOfEternl does not exist; Data/Item/Sword34.bmd is drawn without it " + SwordModels); } TEST_CASE("The item model error message lists the errors only, up to a limit [data][items]") From 9fe1c06c5f6a1cff6b4dd952edbc7310736cc19f Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Wed, 30 Sep 2026 13:08:49 +0200 Subject: [PATCH 7/9] fix(items): second review of the item effects (phase 4c3) - 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. --- .../UI/ItemEditor/ItemEditorLooks.cpp | 17 ++++++++----- .../Data/GameData/ItemData/ItemDatabase.cpp | 1 + .../Data/GameData/ItemData/ItemDatabase.h | 8 +++++++ src/source/Render/Items/ItemRenderStyles.cpp | 12 +++++----- tests/data/test_item_database.cpp | 24 +++++++++++++++++++ 5 files changed, 50 insertions(+), 12 deletions(-) diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp index 2e9556256..b1fdf788d 100644 --- a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp @@ -31,12 +31,14 @@ struct Look std::vector> users; }; -// What the section shows for the selected item, built when the selection or -// the item model data changes (not every frame). +// What the section shows for the selected item, built when the selection, the +// item model data or the items (their names in the lists) change, not every +// frame. struct LooksCache { int itemType = -1; - int databaseVersion = -1; + int modelDatabaseVersion = -1; + int itemDatabaseVersion = -1; std::string glow; Look renderStyle; Look itemEffect; @@ -73,13 +75,16 @@ Look TakeLook(const ItemModelDefinition& model, LookName look) void Refresh(int itemType, const ItemModelDefinition& model) { - const int databaseVersion = g_ItemModelDatabase.GetVersion(); - if (g_cache.itemType == itemType && g_cache.databaseVersion == databaseVersion) + const int modelDatabaseVersion = g_ItemModelDatabase.GetVersion(); + const int itemDatabaseVersion = g_ItemDatabase.GetVersion(); + if (g_cache.itemType == itemType && g_cache.modelDatabaseVersion == modelDatabaseVersion && + g_cache.itemDatabaseVersion == itemDatabaseVersion) { return; } g_cache.itemType = itemType; - g_cache.databaseVersion = databaseVersion; + g_cache.modelDatabaseVersion = modelDatabaseVersion; + g_cache.itemDatabaseVersion = itemDatabaseVersion; g_cache.glow = DescribeGlow(model); g_cache.renderStyle = TakeLook(model, &ItemModelDefinition::renderStyle); g_cache.itemEffect = TakeLook(model, &ItemModelDefinition::itemEffect); diff --git a/src/source/Data/GameData/ItemData/ItemDatabase.cpp b/src/source/Data/GameData/ItemData/ItemDatabase.cpp index c0c70a45d..dbbce0440 100644 --- a/src/source/Data/GameData/ItemData/ItemDatabase.cpp +++ b/src/source/Data/GameData/ItemData/ItemDatabase.cpp @@ -113,6 +113,7 @@ void ItemDatabase::UpdateDisplayName(ItemDefinition& definition) const void ItemDatabase::Store(int itemType, ItemDefinition definition) { + ++m_version; definition.group = GetItemGroup(itemType); definition.number = GetItemNumber(itemType); UpdateDisplayName(definition); diff --git a/src/source/Data/GameData/ItemData/ItemDatabase.h b/src/source/Data/GameData/ItemData/ItemDatabase.h index 9848e6401..c323f8ade 100644 --- a/src/source/Data/GameData/ItemData/ItemDatabase.h +++ b/src/source/Data/GameData/ItemData/ItemDatabase.h @@ -91,6 +91,13 @@ class ItemDatabase int GetExistingItemCount() const { return m_existingItemCount; } + // Counts the changes of the items; views taken from them (e.g. the item + // names the editor lists) take them again when it changes. + int GetVersion() const + { + return m_version; + } + // Item editor changes. Set replaces the item at the definition's // (group, number); an item without names counts as not existing. void Set(const ItemDefinition& definition); @@ -126,6 +133,7 @@ class ItemDatabase std::vector m_ruleData; std::string m_displayLocale{LocalizedString::NeutralLocale}; int m_existingItemCount = 0; + int m_version = 0; }; } // namespace Data::Items diff --git a/src/source/Render/Items/ItemRenderStyles.cpp b/src/source/Render/Items/ItemRenderStyles.cpp index 1d3782087..fdcc0a70a 100644 --- a/src/source/Render/Items/ItemRenderStyles.cpp +++ b/src/source/Render/Items/ItemRenderStyles.cpp @@ -32,9 +32,9 @@ struct RenderStyle { } - // Styles that shine below +3. - constexpr RenderStyle(const char* styleName, StyleFunction recipe, const ShineBelowPlus3& shineBelowPlus3) - : name(styleName), render(recipe), shine(&shineBelowPlus3) + // Styles that shine below +3; the shine is a named constant. + constexpr RenderStyle(const char* styleName, StyleFunction recipe, const ShineBelowPlus3* shineBelowPlus3) + : name(styleName), render(recipe), shine(shineBelowPlus3) { } @@ -1741,10 +1741,10 @@ const RenderStyle RenderStyles[] = { {"guardianShield", RenderGuardianShield}, {"crossShield", RenderCrossShield}, {"oldScroll", RenderOldScroll}, - {"illusionSorcererCovenant", RenderIllusionSorcererCovenant, HarmonyShine}, + {"illusionSorcererCovenant", RenderIllusionSorcererCovenant, &HarmonyShine}, {"scrollOfBlood", RenderScrollOfBlood}, - {"cursedCastleWater", RenderCursedCastleWater, CursedCastleWaterShine}, - {"harmonyShine", RenderPlainly, HarmonyShine}, + {"cursedCastleWater", RenderCursedCastleWater, &CursedCastleWaterShine}, + {"harmonyShine", RenderPlainly, &HarmonyShine}, {"condorFlame", RenderCondorFlame}, {"condorFeather", RenderCondorFeather}, {"deathBeamKnightFlame", RenderThirdClassQuestItem, BITMAP_ITEM_EFFECT_DBSTONE_R}, diff --git a/tests/data/test_item_database.cpp b/tests/data/test_item_database.cpp index 277a69dde..9b2ee9d55 100644 --- a/tests/data/test_item_database.cpp +++ b/tests/data/test_item_database.cpp @@ -211,6 +211,30 @@ TEST_CASE("Swapping items moves them with their ids [data][items]") CHECK(moved->names.GetNeutral() == "Kris"); } +TEST_CASE("The item database counts its changes [data][items]") +{ + ItemDatabase database; + database.Build(std::vector{MakeItem(SwordGroup, 0, "Kris")}); + int version = database.GetVersion(); + + // Lookups change nothing. + CHECK(database.Find(SwordGroup, 0) != nullptr); + CHECK(database.GetLogName(MakeItemType(SwordGroup, 0)) == "Kris (0,0)"); + CHECK(database.GetVersion() == version); + + // Every editor change and every build does. + ItemDefinition changed = *database.Find(SwordGroup, 0); + changed.names = MakeItem(SwordGroup, 0, "Dagger").names; + database.Set(changed); + CHECK(database.GetVersion() != version); + version = database.GetVersion(); + database.Swap(MakeItemType(SwordGroup, 0), MakeItemType(SwordGroup, 5)); + CHECK(database.GetVersion() != version); + version = database.GetVersion(); + database.Build({}); + CHECK(database.GetVersion() != version); +} + TEST_CASE("Item database answers tag, slot and rule questions [data][items]") { ItemDefinition wing = MakeItem(12, 0, "Wings of Elf"); From aa6eb9f6affd87b7ec18b88cf355f44a861d6d5d Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Wed, 30 Sep 2026 22:11:27 +0200 Subject: [PATCH 8/9] fix(items): event items glow by their level (phase 4c3) Found while testing 4c3 next to a build from before the item refactoring; the old build has the same problems: - The Devil's Eye, Key and Invitation glow like twice their level up to +6 again, as in the original client ([0, 2, 4, 6, 8, 10, 12, 13, ...]). A frame-rate change of 2023 had turned "Level *= 2" into "Level *= pow(2, FPS_ANIMATION_FACTOR)", and its fix into "Level /= 2", so +1 to +5 looked the same. - The Blood Bone glows by its level like the Scroll of Archangel: its own drawing (the red chrome layer) ended the drawing before the level glow. It is the render style bloodBone now, drawn in the light of its level with the red chrome on top, instead of an item effect. - The Illusion Sorcerer Covenant glows by its level like the Old Scroll; it glowed like level 0 at every level. Below +3 it keeps its shine. --- docs/item-data.md | 4 +- .../2026-09-25-data-driven-items-design.md | 49 +++++++++++-------- src/bin/Data/Items/Models/Group13_Helper.json | 3 +- src/bin/Data/Items/Models/Group14_Potion.json | 6 +-- src/source/Render/Items/ItemEffects.cpp | 16 ------ src/source/Render/Items/ItemRenderStyles.cpp | 21 ++++++++ tests/data/test_item_model_files.cpp | 17 +++++-- 7 files changed, 69 insertions(+), 47 deletions(-) diff --git a/docs/item-data.md b/docs/item-data.md index 03c52a1d0..6328e1df3 100644 --- a/docs/item-data.md +++ b/docs/item-data.md @@ -365,8 +365,8 @@ before the model is drawn and can: `hideMesh1`), the level potions glow like (`potion`: +7 at every level above 0); - draw the model itself instead of the usual drawing (the Dark Lord's - scrolls, `fruits`, `spirit`, `bloodBone`, `invisibilityCloak`, - `firecracker`, `gmGift`, `meshesPerLevel`). + scrolls, `fruits`, `spirit`, `invisibilityCloak`, `firecracker`, + `gmGift`, `meshesPerLevel`). Items with the same item effect share it. The socket seeds and spheres and zen have no item effect; they glow like level 0 (`"glow": {"level": 0}`), whatever diff --git a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md index 2ff27741c..ab82b09c5 100644 --- a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md +++ b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md @@ -590,29 +590,36 @@ server with original clients (after phases 6 and B). loaded their models); their recipe (textured, then `RENDER_BRIGHT | RENDER_CHROME2`) comes back as a style with their model entries. - - **4c3 Item effects:** `itemEffect` names what the model does before - it is drawn every frame, from the type chain of `RenderPartObjectEffect`: 29 item - branches for 81 items; 28 of them become 27 effects for 42 items in - `Render/Items/ItemEffects.cpp` (moved unchanged; the Siege Potion and - the Contract share one). An effect places sprites, particles and - lightning on bones, changes values of the drawing (a pulsing glow + - **4c3 Item effects:** `itemEffect` names what the model does before it + is drawn every frame, from the type chain of `RenderPartObjectEffect`: + 29 item branches for 81 items; 28 of them become 27 effects for 42 + items in `Render/Items/ItemEffects.cpp` (moved unchanged; the Siege + Potion and the Contract share one). An effect places sprites, particles + and lightning on bones, 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 socket seeds and spheres (the 39 items of the - last branch) and zen need no effect: they glow like level 0 - (`"glow": {"level": 0}`), which is what their level checks did. The - shine of 10 items below +3 (two more shine passes) becomes values of - their render style, which the drawing code applies: 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` (138 styles now). The - effects of the event models of level variants stay in - `RenderPartObjectEffect` (no model entries yet). Which values the - effects take (bones, colors, sizes) comes with phase 13, when what - the named looks are made of moves into data (D25). MuEditor gets a - read-only view of the looks above the item table: for the selected - item its model file, glow, render style and effect, and for a look - all items that use it (D26). It only reads the model data, so it - needs no change to the drawing code. + last branch) and zen need no effect: they glow like level 0 (`"glow": + {"level": 0}`), which is what their level checks did. The shine of 10 + items below +3 (two more shine passes) becomes values of their render + style, which the drawing code applies: 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` (138 styles now). + Fixes found while testing, which change the look: the Devil's Eye, Key + and Invitation glow like twice their level up to +6 again, as in the + original client (a frame-rate change of 2023 had turned `Level *= 2` + into `Level *= pow(2, …)`, and its fix into `Level /= 2`, so +1 to +5 + looked the same); the Blood Bone and the Illusion Sorcerer Covenant + glow by their level like the Scroll of Archangel and the Old Scroll + (the Blood Bone's own drawing ended the drawing before the level glow; + it is the render style `bloodBone` now, which leaves 26 effects for 41 + items and 139 styles; the Covenant glowed like level 0). The effects of + the event models of level variants stay in `RenderPartObjectEffect` (no + model entries yet). Which values the effects take (bones, colors, + sizes) comes with phase 13, when what the named looks are made of moves + into data (D25). MuEditor gets a read-only view of the looks above the + item table: for the selected item its model file, glow, render style + and effect, and for a look all items that use it (D26). It only reads + the model data, so it needs no change to the drawing code. Verified like phase 3: one-time comparisons of the old and the new code (which files and texture folders are loaded for each model; the diff --git a/src/bin/Data/Items/Models/Group13_Helper.json b/src/bin/Data/Items/Models/Group13_Helper.json index 02f962d9b..e58dc0501 100644 --- a/src/bin/Data/Items/Models/Group13_Helper.json +++ b/src/bin/Data/Items/Models/Group13_Helper.json @@ -171,7 +171,7 @@ "rotation": [-45, -5, 18], "scale": 0.48 }, - "itemEffect": "bloodBone" + "renderStyle": "bloodBone" }, { "number": 18, @@ -578,7 +578,6 @@ "scale": 0.6 }, "glow": { - "level": 0, "shineHiddenMesh": 1 }, "renderStyle": "illusionSorcererCovenant" diff --git a/src/bin/Data/Items/Models/Group14_Potion.json b/src/bin/Data/Items/Models/Group14_Potion.json index 1940cb813..088c4df7e 100644 --- a/src/bin/Data/Items/Models/Group14_Potion.json +++ b/src/bin/Data/Items/Models/Group14_Potion.json @@ -196,7 +196,7 @@ "rotation": [90, 0, -45] }, "glow": { - "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] + "level": [0, 2, 4, 6, 8, 10, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, "itemEffect": "devilsEye" }, @@ -212,7 +212,7 @@ "rotation": [270, 0, 270] }, "glow": { - "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] + "level": [0, 2, 4, 6, 8, 10, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, "itemEffect": "devilsKey" }, @@ -228,7 +228,7 @@ "rotation": [270, 0, 90] }, "glow": { - "level": [0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13] + "level": [0, 2, 4, 6, 8, 10, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, "itemEffect": "devilsInvitation" }, diff --git a/src/source/Render/Items/ItemEffects.cpp b/src/source/Render/Items/ItemEffects.cpp index 73e56b2ca..ee54bc260 100644 --- a/src/source/Render/Items/ItemEffects.cpp +++ b/src/source/Render/Items/ItemEffects.cpp @@ -159,21 +159,6 @@ Result ApplyFruits(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) return Result::Drawn; } -Result ApplyBloodBone(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) -{ - o->BlendMeshTexCoordU = sinf(gMapManager.WorldActive * 0.0001f); - o->BlendMeshTexCoordV = -WorldTime * 0.0005f; - Vector(.9f, .9f, .9f, b->BodyLight); - b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, - o->BlendMeshTexCoordV, o->HiddenMesh); - Vector(.9f, .1f, .1f, b->BodyLight); - b->StreamMesh = 0; - b->RenderBody(RENDER_TEXTURE | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, - o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME); - b->StreamMesh = -1; - return Result::Drawn; -} - Result ApplyInvisibilityCloak(BMD* b, OBJECT* o, int Type, float Alpha, int& Level) { Vector(0.8f, 0.8f, 0.8f, b->BodyLight); @@ -561,7 +546,6 @@ const ItemEffect ItemEffects[] = { {"spirit", ApplySpirit}, {"potion", ApplyPotion}, {"fruits", ApplyFruits}, - {"bloodBone", ApplyBloodBone}, {"invisibilityCloak", ApplyInvisibilityCloak}, {"devilsEye", ApplyDevilsEye}, {"devilsKey", ApplyDevilsKey}, diff --git a/src/source/Render/Items/ItemRenderStyles.cpp b/src/source/Render/Items/ItemRenderStyles.cpp index fdcc0a70a..79183a8a2 100644 --- a/src/source/Render/Items/ItemRenderStyles.cpp +++ b/src/source/Render/Items/ItemRenderStyles.cpp @@ -1690,6 +1690,26 @@ bool RenderLuckyItem(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType) return true; } +// The bone in the light of its level (a little darker), then a red chrome +// layer running over it; the level glow comes on top as for other items. +bool RenderBloodBone(BMD* b, OBJECT* o, int Type, float Alpha, int RenderType) +{ + o->BlendMeshTexCoordU = sinf(gMapManager.WorldActive * 0.0001f); + o->BlendMeshTexCoordV = -WorldTime * 0.0005f; + vec3_t light; + VectorCopy(b->BodyLight, light); + Vector(light[0] * 0.9f, light[1] * 0.9f, light[2] * 0.9f, b->BodyLight); + b->RenderBody(RENDER_TEXTURE, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh); + Vector(.9f, .1f, .1f, b->BodyLight); + b->StreamMesh = 0; + b->RenderBody(RENDER_TEXTURE | RENDER_BRIGHT, o->Alpha, o->BlendMesh, o->BlendMeshLight, o->BlendMeshTexCoordU, + o->BlendMeshTexCoordV, o->HiddenMesh, BITMAP_CHROME); + b->StreamMesh = -1; + VectorCopy(light, b->BodyLight); + return true; +} + // ------------------------------------------------ glow passes // The whole staff glows, then its second mesh again, white; the object keeps @@ -1742,6 +1762,7 @@ const RenderStyle RenderStyles[] = { {"crossShield", RenderCrossShield}, {"oldScroll", RenderOldScroll}, {"illusionSorcererCovenant", RenderIllusionSorcererCovenant, &HarmonyShine}, + {"bloodBone", RenderBloodBone}, {"scrollOfBlood", RenderScrollOfBlood}, {"cursedCastleWater", RenderCursedCastleWater, &CursedCastleWaterShine}, {"harmonyShine", RenderPlainly, &HarmonyShine}, diff --git a/tests/data/test_item_model_files.cpp b/tests/data/test_item_model_files.cpp index ac1ab856e..a8ba0b78f 100644 --- a/tests/data/test_item_model_files.cpp +++ b/tests/data/test_item_model_files.cpp @@ -454,7 +454,10 @@ TEST_CASE("Shipped item models keep the glow of the old drawing code [data][item CHECK(glowOf(4, 15).levels == GlowLevels{0, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31}); // The Devil's Square items glow like half their square, the seventh // square like +13. - CHECK(glowOf(14, 17).levels == GlowLevels{0, 0, 1, 1, 2, 2, 3, 13, 13, 13, 13, 13, 13, 13, 13, 13}); + // The Devil's Square items glow like twice their level up to +6 and like + // +13 from +7, as in the original client (a frame-rate change of 2023 had + // turned the doubling into a halving). + CHECK(glowOf(14, 17).levels == GlowLevels{0, 2, 4, 6, 8, 10, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13}); CHECK_FALSE(glowOf(12, 0).excellent); CHECK_FALSE(glowOf(13, 30).excellent); } @@ -502,8 +505,13 @@ TEST_CASE("Models drawn for items glow like the old drawing code [data][items]") CHECK(GetLevel(MODEL_ARROWS, 0) == 0); CHECK(GetLevel(MODEL_ARROWS, 3) == 7); - CHECK(GetLevel(MODEL_DEVILS_EYE, 5) == 2); + CHECK(GetLevel(MODEL_DEVILS_EYE, 1) == 2); + CHECK(GetLevel(MODEL_DEVILS_EYE, 5) == 10); CHECK(GetLevel(MODEL_DEVILS_EYE, 7) == 13); + // The Blood Bone and the Illusion Sorcerer Covenant glow by their level, + // like the Scroll of Archangel and the Old Scroll. + CHECK(GetLevel(MODEL_ITEM + MakeItemType(13, 17), 5) == 5); + CHECK(GetLevel(MODEL_ITEM + MakeItemType(13, 50), 5) == 5); // The event models of level variants stay in code. CHECK(GetLevel(MODEL_EVENT + 14, 2) == 9); CHECK(GetLevel(MODEL_ITEM + MakeItemType(14, 13), 0) == 8); @@ -571,6 +579,7 @@ TEST_CASE("Shipped item models keep the looks of the old drawing code [data][ite CHECK(styleOf(8, 9) == "helperNpcPlate"); // The Deadly Staff also glows in its own way. CHECK(styleOf(5, 30) == "deadlyStaff"); + CHECK(styleOf(13, 17) == "bloodBone"); } TEST_CASE("Render styles that are not for every drawing leave it to the drawing code [data][items]") @@ -747,8 +756,10 @@ TEST_CASE("Shipped item models keep the item effects of the old drawing code [da // Items with the same code share it. CHECK(effectOf(14, 7) == "hiddenMeshByLevel"); CHECK(effectOf(13, 7) == "hiddenMeshByLevel"); - // The shine below +3 is part of the render style. + // The shine below +3 is part of the render style; so is the red chrome of + // the Blood Bone, which glows by its level. CHECK(effectOf(13, 43).empty()); + CHECK(effectOf(13, 17).empty()); CHECK(effectOf(14, 64).empty()); // The socket seeds and spheres and zen glow like level 0, whatever their From 283d6321592d77dedab901d9e20709b57bc25303 Mon Sep 17 00:00:00 2001 From: Mosch0512 Date: Wed, 30 Sep 2026 22:17:22 +0200 Subject: [PATCH 9/9] fix(items): the Devil's Square items glow in pairs of levels (phase 4c3) In game, +1 and +2, +3 and +4, +5 and +6 of the Devil's Eye, Key and Invitation look alike and +7 has its own look. The level glow changes at +3, +5 and +7, so the items glow like their own level up to +6 and like +13 from +7: [0, 1, 2, 3, 4, 5, 6, 13, ...]. The doubling of the original code gave every level its own step. --- .../2026-09-25-data-driven-items-design.md | 31 ++++++++++--------- src/bin/Data/Items/Models/Group14_Potion.json | 6 ++-- tests/data/test_item_model_files.cpp | 12 +++---- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md index ab82b09c5..a7cbec1ae 100644 --- a/docs/superpowers/specs/2026-09-25-data-driven-items-design.md +++ b/docs/superpowers/specs/2026-09-25-data-driven-items-design.md @@ -605,21 +605,22 @@ server with original clients (after phases 6 and B). Covenant and the Cursed Castle water have a style already, the Jewel of Harmony and the Moonstone Pendant get `harmonyShine` (138 styles now). Fixes found while testing, which change the look: the Devil's Eye, Key - and Invitation glow like twice their level up to +6 again, as in the - original client (a frame-rate change of 2023 had turned `Level *= 2` - into `Level *= pow(2, …)`, and its fix into `Level /= 2`, so +1 to +5 - looked the same); the Blood Bone and the Illusion Sorcerer Covenant - glow by their level like the Scroll of Archangel and the Old Scroll - (the Blood Bone's own drawing ended the drawing before the level glow; - it is the render style `bloodBone` now, which leaves 26 effects for 41 - items and 139 styles; the Covenant glowed like level 0). The effects of - the event models of level variants stay in `RenderPartObjectEffect` (no - model entries yet). Which values the effects take (bones, colors, - sizes) comes with phase 13, when what the named looks are made of moves - into data (D25). MuEditor gets a read-only view of the looks above the - item table: for the selected item its model file, glow, render style - and effect, and for a look all items that use it (D26). It only reads - the model data, so it needs no change to the drawing code. + and Invitation glow like their level up to +6, so +1 and +2, +3 and +4, + +5 and +6 look alike, and like +13 from +7 (the original client doubled + the level, a frame-rate change of 2023 turned that into `Level *= + pow(2, …)` and its fix into `Level /= 2`, which gave +1 to +5 one + look); the Blood Bone and the Illusion Sorcerer Covenant glow by their + level like the Scroll of Archangel and the Old Scroll (the Blood Bone's + own drawing ended the drawing before the level glow; it is the render + style `bloodBone` now, which leaves 26 effects for 41 items and 139 + styles; the Covenant glowed like level 0). The effects of the event + models of level variants stay in `RenderPartObjectEffect` (no model + entries yet). Which values the effects take (bones, colors, sizes) + comes with phase 13, when what the named looks are made of moves into + data (D25). MuEditor gets a read-only view of the looks above the item + table: for the selected item its model file, glow, render style and + effect, and for a look all items that use it (D26). It only reads the + model data, so it needs no change to the drawing code. Verified like phase 3: one-time comparisons of the old and the new code (which files and texture folders are loaded for each model; the diff --git a/src/bin/Data/Items/Models/Group14_Potion.json b/src/bin/Data/Items/Models/Group14_Potion.json index 088c4df7e..02aac9e9c 100644 --- a/src/bin/Data/Items/Models/Group14_Potion.json +++ b/src/bin/Data/Items/Models/Group14_Potion.json @@ -196,7 +196,7 @@ "rotation": [90, 0, -45] }, "glow": { - "level": [0, 2, 4, 6, 8, 10, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13] + "level": [0, 1, 2, 3, 4, 5, 6, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, "itemEffect": "devilsEye" }, @@ -212,7 +212,7 @@ "rotation": [270, 0, 270] }, "glow": { - "level": [0, 2, 4, 6, 8, 10, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13] + "level": [0, 1, 2, 3, 4, 5, 6, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, "itemEffect": "devilsKey" }, @@ -228,7 +228,7 @@ "rotation": [270, 0, 90] }, "glow": { - "level": [0, 2, 4, 6, 8, 10, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13] + "level": [0, 1, 2, 3, 4, 5, 6, 13, 13, 13, 13, 13, 13, 13, 13, 13] }, "itemEffect": "devilsInvitation" }, diff --git a/tests/data/test_item_model_files.cpp b/tests/data/test_item_model_files.cpp index a8ba0b78f..ce5b5794c 100644 --- a/tests/data/test_item_model_files.cpp +++ b/tests/data/test_item_model_files.cpp @@ -454,10 +454,10 @@ TEST_CASE("Shipped item models keep the glow of the old drawing code [data][item CHECK(glowOf(4, 15).levels == GlowLevels{0, 3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23, 25, 27, 29, 31}); // The Devil's Square items glow like half their square, the seventh // square like +13. - // The Devil's Square items glow like twice their level up to +6 and like - // +13 from +7, as in the original client (a frame-rate change of 2023 had - // turned the doubling into a halving). - CHECK(glowOf(14, 17).levels == GlowLevels{0, 2, 4, 6, 8, 10, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13}); + // The Devil's Square items glow like their level up to +6, so +1 and +2, + // +3 and +4, +5 and +6 look alike (the level glow changes at +3, +5 and + // +7), and like +13 from +7. + CHECK(glowOf(14, 17).levels == GlowLevels{0, 1, 2, 3, 4, 5, 6, 13, 13, 13, 13, 13, 13, 13, 13, 13}); CHECK_FALSE(glowOf(12, 0).excellent); CHECK_FALSE(glowOf(13, 30).excellent); } @@ -505,8 +505,8 @@ TEST_CASE("Models drawn for items glow like the old drawing code [data][items]") CHECK(GetLevel(MODEL_ARROWS, 0) == 0); CHECK(GetLevel(MODEL_ARROWS, 3) == 7); - CHECK(GetLevel(MODEL_DEVILS_EYE, 1) == 2); - CHECK(GetLevel(MODEL_DEVILS_EYE, 5) == 10); + CHECK(GetLevel(MODEL_DEVILS_EYE, 1) == 1); + CHECK(GetLevel(MODEL_DEVILS_EYE, 6) == 6); CHECK(GetLevel(MODEL_DEVILS_EYE, 7) == 13); // The Blood Bone and the Illusion Sorcerer Covenant glow by their level, // like the Scroll of Archangel and the Old Scroll.