diff --git a/docs/item-data.md b/docs/item-data.md index 649646789c..6328e1df3b 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. | +| `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 @@ -339,6 +340,41 @@ 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). +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`. + +`itemEffect` names what the model does before it is drawn: + +```json +{ "number": 37, "file": "Data/Item/wing09.bmd", "textureFolders": ["Item"], "itemEffect": "wingOfEternal" } +``` + +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 + 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 at every level + above 0); +- draw the model itself instead of the usual drawing (the Dark Lord's + 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 +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 +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 | +| `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 @@ -417,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 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. + ### 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 205331c980..a7cbec1aea 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 @@ -54,7 +56,9 @@ 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 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. | ## Current state @@ -378,7 +382,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) | 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. | @@ -397,11 +402,11 @@ 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). | -| 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) 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. | @@ -411,7 +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. | -| 13 | Cleanup | Client | MuMain | all | The glow of the Phoenix Soul inventory model, then remove this document. | +| 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). | +| 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 @@ -525,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", "itemEffect": "flameSparks" } ``` Every item keeps its model slot `MODEL_ITEM + item type` (D23, own model slots). Three @@ -557,7 +564,7 @@ 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: + 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 @@ -583,8 +590,37 @@ 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 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). + Fixes found while testing, which change the look: the Devil's Eye, Key + 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 @@ -620,7 +656,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. +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. @@ -740,7 +780,20 @@ 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 their names from the effect + 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 for an item that glows differently from its item: only on its first @@ -749,8 +802,19 @@ 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. + - Whatever later areas still need from this document moves to the + roadmap, so it is not lost. - Remove this document. +## 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 - **Q1: Custom items on the original client** *(deferred; must be 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 0000000000..6da5f4d85e --- /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 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 + +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/`: 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. | +| 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. diff --git a/src/Localization/Editor.en.resx b/src/Localization/Editor.en.resx index e8b6d4efb1..8a1a90d6ea 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 + + + Item 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 0000000000..b1fdf788de --- /dev/null +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.cpp @@ -0,0 +1,150 @@ +#include "stdafx.h" + +#ifdef _EDITOR + +#include "ItemEditorLooks.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 +#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, the +// item model data or the items (their names in the lists) change, not every +// frame. +struct LooksCache +{ + int itemType = -1; + int modelDatabaseVersion = -1; + int itemDatabaseVersion = -1; + std::string glow; + Look renderStyle; + Look itemEffect; +}; + +LooksCache g_cache; + +// 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() : "-"; +} + +Look TakeLook(const ItemModelDefinition& model, LookName look) +{ + 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 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.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); +} + +// 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()) + { + ImGui::Text("%s: -", label); + return -1; + } + + int clicked = -1; + ImGui::PushID(label); + if (ImGui::TreeNode("look", "%s: %s (%s %d)", label, look.name.c_str(), I18N::Editor::UsedBy, + static_cast(look.users.size()))) + { + 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)) + { + for (const auto& [itemType, name] : look.users) + { + if (ImGui::Selectable(name.c_str(), itemType == g_cache.itemType)) + { + clicked = itemType; + } + } + ImGui::EndListBox(); + } + ImGui::TreePop(); + } + ImGui::PopID(); + return clicked; +} +} // namespace + +int CItemEditorLooks::Render(int itemType) +{ + if (!ImGui::CollapsingHeader(I18N::Editor::Looks)) + { + return -1; + } + + const ItemModelDefinition* model = g_ItemModelDatabase.Find(itemType); + if (model == nullptr) + { + ImGui::TextDisabled("%s", itemType < 0 ? I18N::Editor::SelectAnItem : I18N::Editor::NoModel); + return -1; + } + Refresh(itemType, *model); + 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::ItemEffect, g_cache.itemEffect); + return clickedStyleUser >= 0 ? clickedStyleUser : clickedEffectUser; +} + +#endif // _EDITOR diff --git a/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h new file mode 100644 index 0000000000..d7c68f95d5 --- /dev/null +++ b/src/MuEditor/UI/ItemEditor/ItemEditorLooks.h @@ -0,0 +1,17 @@ +#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 item effect, and the items that share a +// look. +class CItemEditorLooks +{ +public: + // Shown above the item table; `itemType` is the selected item, -1 for none. + // 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 462fa72637..85816491ae 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 02db95ef7d..4277a0ffd7 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 9a9ca9bd0b..19d7778e98 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,14 @@ void CMuItemEditorUI::Render(bool& showEditor) ImGui::Checkbox(I18N::Editor::FreezeIndexName, &m_bFreezeColumns); ImGui::Separator(); + // 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; std::transform(searchLower.begin(), searchLower.end(), searchLower.begin(), ::tolower); @@ -173,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 630c0b2c75..da09482760 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/Group03_Spear.json b/src/bin/Data/Items/Models/Group03_Spear.json index 24d05e895c..8e5e16c105 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" - } + }, + "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 98c7fc2d72..4b58f3ac3a 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", + "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 a7b2f02bd6..8cd2092dcd 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" - } + }, + "itemEffect": "divineSet" }, { "number": 21, @@ -340,7 +341,8 @@ }, "glow": { "color": "copper" - } + }, + "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 23c60acd1f..8857424f4b 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" - } + }, + "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 7ddf47cb9f..ee5bf5ca5c 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" - } + }, + "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 3a2c243b19..f4dcdf0515 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" - } + }, + "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 050768b821..40be7bf27b 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 - } + }, + "itemEffect": "wingsOfSoul" }, { "number": 5, @@ -93,7 +94,8 @@ "glow": { "level": 0, "excellent": false - } + }, + "itemEffect": "wingsOfDragon" }, { "number": 6, @@ -110,7 +112,8 @@ "level": 0, "excellent": false }, - "renderStyle": "wingsOfDarkness" + "renderStyle": "wingsOfDarkness", + "itemEffect": "wingsOfDarkness" }, { "number": 7, @@ -319,7 +322,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "itemEffect": "darkLordScroll" }, { "number": 22, @@ -330,7 +334,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "itemEffect": "darkLordScroll" }, { "number": 23, @@ -341,7 +346,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "itemEffect": "darkLordScroll" }, { "number": 24, @@ -352,7 +358,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "itemEffect": "darkLordScroll" }, { "number": 30, @@ -432,7 +439,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "itemEffect": "darkLordScroll" }, { "number": 36, @@ -448,7 +456,8 @@ "level": 0, "excellent": false }, - "renderStyle": "wingOfStorm" + "renderStyle": "wingOfStorm", + "itemEffect": "wingOfStorm" }, { "number": 37, @@ -464,7 +473,8 @@ "glow": { "level": 0, "excellent": false - } + }, + "itemEffect": "wingOfEternal" }, { "number": 38, @@ -480,7 +490,8 @@ "glow": { "level": 0, "excellent": false - } + }, + "itemEffect": "wingOfIllusion" }, { "number": 39, @@ -498,7 +509,8 @@ "level": 0, "excellent": false }, - "renderStyle": "wingOfRuin" + "renderStyle": "wingOfRuin", + "itemEffect": "wingOfRuin" }, { "number": 40, @@ -565,7 +577,8 @@ "level": 0, "excellent": false }, - "renderStyle": "wingOfDimension" + "renderStyle": "wingOfDimension", + "itemEffect": "wingOfDimension" }, { "number": 44, @@ -640,7 +653,8 @@ }, "ground": { "rotation": [270, 0, 45] - } + }, + "itemEffect": "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 8ae07207ce..e58dc05018 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", + "itemEffect": "hiddenMeshByLevel" }, { "number": 8, @@ -102,7 +103,8 @@ "inventory": { "anchor": [0.5, 0.8] }, - "renderStyle": "lifeStone" + "renderStyle": "lifeStone", + "itemEffect": "hideMesh1" }, { "number": 12, @@ -141,7 +143,8 @@ "inventory": { "anchor": [0.6, 1], "scale": 0.003 - } + }, + "itemEffect": "fruits" }, { "number": 16, @@ -167,7 +170,8 @@ "ground": { "rotation": [-45, -5, 18], "scale": 0.48 - } + }, + "renderStyle": "bloodBone" }, { "number": 18, @@ -181,7 +185,8 @@ "ground": { "rotation": [165, -168, 198], "scale": 0.48 - } + }, + "itemEffect": "invisibilityCloak" }, { "number": 21, @@ -302,7 +307,8 @@ "inventory": { "anchor": [0.5, 0.9], "rotation": [-90, -90, 0] - } + }, + "itemEffect": "spirit" }, { "number": 32, @@ -395,7 +401,8 @@ "anchor": [0.5, 0.7], "offset": [0, 0.02], "rotation": [-198, 0, 0] - } + }, + "renderStyle": "harmonyShine" }, { "number": 39, @@ -571,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 3d462a0f3a..02aac9e9c0 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 - } + }, + "itemEffect": "potion" }, { "number": 1, @@ -18,7 +19,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "itemEffect": "potion" }, { "number": 2, @@ -27,7 +29,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "itemEffect": "potion" }, { "number": 3, @@ -36,7 +39,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "itemEffect": "potion" }, { "number": 4, @@ -45,7 +49,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "itemEffect": "potion" }, { "number": 5, @@ -54,7 +59,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "itemEffect": "potion" }, { "number": 6, @@ -63,7 +69,8 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 - } + }, + "itemEffect": "potion" }, { "number": 7, @@ -72,7 +79,8 @@ "inventory": { "anchor": [0.5, 0.5] }, - "renderStyle": "siegePotion" + "renderStyle": "siegePotion", + "itemEffect": "hiddenMeshByLevel" }, { "number": 8, @@ -156,6 +164,9 @@ "inventory": { "anchor": [0.5, 0.95], "scale": 0.0035 + }, + "glow": { + "level": 0 } }, { @@ -185,8 +196,9 @@ "rotation": [90, 0, -45] }, "glow": { - "level": [0, 0, 1, 1, 2, 2, 3, 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" }, { "number": 18, @@ -200,8 +212,9 @@ "rotation": [270, 0, 270] }, "glow": { - "level": [0, 0, 1, 1, 2, 2, 3, 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" }, { "number": 19, @@ -215,8 +228,9 @@ "rotation": [270, 0, 90] }, "glow": { - "level": [0, 0, 1, 1, 2, 2, 3, 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" }, { "number": 20, @@ -245,7 +259,8 @@ }, "ground": { "rotation": [270, 0, 90] - } + }, + "itemEffect": "rena" }, { "number": 22, @@ -321,7 +336,8 @@ "anchor": [0.5, 0.95], "rotation": [270, 0, 0], "scale": 0.0035 - } + }, + "itemEffect": "meshesPerLevel" }, { "number": 28, @@ -486,7 +502,8 @@ }, "glow": { "level": 0 - } + }, + "renderStyle": "harmonyShine" }, { "number": 43, @@ -624,7 +641,8 @@ "ground": { "rotation": [0, 0, -10], "scale": 0.4 - } + }, + "itemEffect": "gmGift" }, { "number": 53, @@ -750,7 +768,8 @@ "ground": { "rotation": [70, 0, -45], "scale": 1.5 - } + }, + "itemEffect": "firecracker" }, { "number": 64, diff --git a/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp b/src/source/Data/DataHandler/ItemData/ItemModelLoader.cpp index 135fa321dc..98220e35fa 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.itemEffect, lookNames.itemEffect, ItemModelProblemType::ItemEffectUnknown); } 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 b255a65d09..1cc6d37c08 100644 --- a/src/source/Data/DataHandler/ItemData/ItemModelLoader.h +++ b/src/source/Data/DataHandler/ItemData/ItemModelLoader.h @@ -8,13 +8,26 @@ // 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); +// 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::ItemEffects::Exists). +struct LookNames +{ + LookExists renderStyle = NoLookExists; + LookExists itemEffect = NoLookExists; +}; + +// Opens the .bmd file of every item model; `lookNames` checks the names of +// the "renderStyle" and "itemEffect" 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 3d8521db6c..41b2da9cdd 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::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 aad788bb78..82e218a60d 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, + // "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. @@ -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, ItemEffectUnknown: 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/ItemDatabase.cpp b/src/source/Data/GameData/ItemData/ItemDatabase.cpp index c0c70a45d5..dbbce0440b 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 9848e6401f..c323f8aded 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/Data/GameData/ItemData/ItemModelDefinition.h b/src/source/Data/GameData/ItemData/ItemModelDefinition.h index dd26a96dea..380e66ad07 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 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 itemEffect; bool Exists() const { diff --git a/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp b/src/source/Data/GameData/ItemData/ItemModelJsonFormat.cpp index c6d922952d..7689efd2c9 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* ItemEffect = "itemEffect"; } // namespace Keys constexpr std::string_view ModelFileExtension = ".bmd"; @@ -86,6 +87,10 @@ OrderedJson WriteModel(const ItemModelDefinition& model) { json[Keys::RenderStyle] = model.renderStyle; } + if (!model.itemEffect.empty()) + { + json[Keys::ItemEffect] = model.itemEffect; + } 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::ItemEffect}; 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::ItemEffect, model.itemEffect); WarnAboutUnknownKeys(json); return !m_hasErrors; } diff --git a/src/source/Engine/Object/ZzzObject.cpp b/src/source/Engine/Object/ZzzObject.cpp index d20433603a..9d67852844 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::ItemEffects::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::ItemEffects::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,96 @@ 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); + return Result::None; +} - float Scale = Luminosity * 0.8f; - Vector(Luminosity * 2, Luminosity * 0.32f, Luminosity * 0.32f, EffLight); +// Items have the item effect of their model entry; the event models of level +// variants have theirs in ApplyEventModelEffect. +Render::Items::ItemEffects::Result ApplyPartObjectEffect(BMD* b, OBJECT* o, int Type, float Alpha, int& Level, + int 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); +} - 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); - } - } - else if (Type == MODEL_WING_OF_DIMENSION) + if (ApplyPartObjectEffect(b, o, Type, Alpha, Level, ItemLevel) == Render::Items::ItemEffects::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 +6853,28 @@ 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; 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); - 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) + else if (shine->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); + const float factor = *shine->light; + Vector(Light[0] * factor, Light[1] * factor, Light[2] * factor, b->BodyLight); } - 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 + RenderPartObjectBody(b, o, Type, Alpha, RenderType); + if (shine != nullptr) + { + for (const Render::Items::Styles::ShineBelowPlus3::Pass& pass : shine->passes) { - VectorCopy(Light, b->BodyLight); - RenderPartObjectBody(b, o, Type, Alpha, RenderType); + 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 d394a7f9e2..559316c09b 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 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::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 f8afb0b6de..6f86c83db3 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::ItemEffects::Exists}); PrepareGrandSoulPantsClothMesh(); } OpenItemEffectModels(); diff --git a/src/source/Render/Items/ItemEffects.cpp b/src/source/Render/Items/ItemEffects.cpp new file mode 100644 index 0000000000..ee54bc2605 --- /dev/null +++ b/src/source/Render/Items/ItemEffects.cpp @@ -0,0 +1,597 @@ +#include "stdafx.h" + +#include "ItemEffects.h" +#include "ItemModelTable.h" + +#include "Core/Globals/_enum.h" +#include "Engine/Object/w_ObjectInfo.h" +#include "Engine/Object/ZzzCharacter.h" +#include "Render/Effects/ZzzEffect.h" +#include "Render/Models/ZzzBMD.h" + +#include +#include + +namespace Render::Items::ItemEffects +{ +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); + +struct ItemEffect +{ + const char* name; + EffectFunction apply; +}; + +// ------------------------------------------------ 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); + 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); + 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); + 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); + break; + } + 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 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 names + +const ItemEffect ItemEffects[] = { + {"billOfBalrog", ApplyBillOfBalrog}, + {"meshesPerLevel", ApplyMeshesPerLevel}, + {"firecracker", ApplyFirecracker}, + {"gmGift", ApplyGmGift}, + {"darkLordScroll", ApplyDarkLordScroll}, + {"spirit", ApplySpirit}, + {"potion", ApplyPotion}, + {"fruits", ApplyFruits}, + {"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}, +}; + +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.itemEffect.empty() ? nullptr + : FindEffect(model.itemEffect); + }}; +} // 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); + return effect != nullptr ? effect->apply(b, o, modelType, alpha, level) : Result::None; +} +} // namespace Render::Items::ItemEffects diff --git a/src/source/Render/Items/ItemEffects.h b/src/source/Render/Items/ItemEffects.h new file mode 100644 index 0000000000..f4ec0883f2 --- /dev/null +++ b/src/source/Render/Items/ItemEffects.h @@ -0,0 +1,33 @@ +#pragma once + +#include + +class BMD; +class OBJECT; + +// 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, "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. +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 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::ItemEffects diff --git a/src/source/Render/Items/ItemModelTable.h b/src/source/Render/Items/ItemModelTable.h new file mode 100644 index 0000000000..d2cccd3986 --- /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 a13b2beba3..79183a8a2b 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; the shine is a named constant. + 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, @@ -1668,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 @@ -1719,9 +1761,11 @@ const RenderStyle RenderStyles[] = { {"guardianShield", RenderGuardianShield}, {"crossShield", RenderCrossShield}, {"oldScroll", RenderOldScroll}, - {"illusionSorcererCovenant", RenderIllusionSorcererCovenant}, + {"illusionSorcererCovenant", RenderIllusionSorcererCovenant, &HarmonyShine}, + {"bloodBone", RenderBloodBone}, {"scrollOfBlood", RenderScrollOfBlood}, - {"cursedCastleWater", RenderCursedCastleWater}, + {"cursedCastleWater", RenderCursedCastleWater, &CursedCastleWaterShine}, + {"harmonyShine", RenderPlainly, &HarmonyShine}, {"condorFlame", RenderCondorFlame}, {"condorFeather", RenderCondorFeather}, {"deathBeamKnightFlame", RenderThirdClassQuestItem, BITMAP_ITEM_EFFECT_DBSTONE_R}, @@ -1736,9 +1780,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}, @@ -1824,16 +1868,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 +1875,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 +1893,19 @@ 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); } +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 = FindItemStyle(modelType); + const RenderStyle* style = g_itemStyles.Find(modelType); if (style == nullptr || style->glow == nullptr) { return false; diff --git a/src/source/Render/Items/ItemRenderStyles.h b/src/source/Render/Items/ItemRenderStyles.h index 187ac19a5f..001d0c50b0 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_database.cpp b/tests/data/test_item_database.cpp index 277a69ddec..9b2ee9d550 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"); diff --git a/tests/data/test_item_model_files.cpp b/tests/data/test_item_model_files.cpp index 51ae9a5872..ce5b5794c6 100644 --- a/tests/data/test_item_model_files.cpp +++ b/tests/data/test_item_model_files.cpp @@ -13,8 +13,10 @@ #include "Data/GameData/ItemData/ItemModelJsonFormat.h" #include "Data/GameData/ItemData/ItemTextureFiles.h" #include "Data/GameData/ItemData/ItemType.h" -#include "Render/Items/ItemDisplay.h" +#include "Engine/Object/ZzzObject.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" @@ -26,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -451,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 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); } @@ -499,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) == 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. + 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); @@ -568,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]") @@ -709,3 +721,174 @@ 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 item effects of shipped item models exist [data][items]") +{ + for (const ItemModelDefinition& model : ShippedModels().models) + { + if (!model.itemEffect.empty()) + { + INFO("(" << model.group << "," << model.number << ") " << model.itemEffect); + CHECK(Render::Items::ItemEffects::Exists(model.itemEffect)); + } + } + 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 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->itemEffect; + }; + + 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 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 + // 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 ItemEffects::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 ItemEffects::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(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{}); + 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 item effect of a model is the one of its item, or of its event model [data][items]") +{ + using Render::Items::ItemEffects::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{}); +} diff --git a/tests/data/test_item_model_json.cpp b/tests/data/test_item_model_json.cpp index f93f09bc4d..1338af190a 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.itemEffect = "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 item effect of a model is a name [data][items]") +{ + const std::string text = WriteItemModelGroupJson(13, std::vector{MakeDarkHorse()}); + 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", "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 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].itemEffect.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 f4009ffc1d..fcfdcf6fbb 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 item effect that does not exist is a warning [data][items]") +{ + ItemModelProblem problem = MakeMissingTexture(0, ""); + problem.type = ItemModelProblemType::ItemEffectUnknown; + problem.name = "wingOfEternl"; + + CHECK_FALSE(problem.IsError()); + CHECK(problem.ToLogString() == + "(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]") { const ItemDatabase items = MakeItems();