diff --git a/.gitignore b/.gitignore index 56bcc36531..7545d352fe 100644 --- a/.gitignore +++ b/.gitignore @@ -309,6 +309,8 @@ public/icons/* !public/icons/items/** !public/icons/buildables/ !public/icons/buildables/** +!public/icons/npcs/ +!public/icons/npcs/** # Ignore Python scripts /*.py diff --git a/AGENTS.md b/AGENTS.md index 85692b32d3..3ddca9422d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,6 +5,7 @@ - repo-owned curated ability icons under `public/icons/abilities/` (typically via `npm run refresh:db-assets`) - repo-owned curated item icons under `public/icons/items/` (typically via `npm run refresh:db-assets`) - repo-owned curated buildable portrait icons under `public/icons/buildables/` (typically via `npm run refresh:db-assets`) + - repo-owned curated NPC/V Blood portrait icons under `public/icons/npcs/` when backed by `data/enrichment/npc-portrait-map.json` and materialized via `npm run refresh:db-assets` - approved Data Base branding assets under `static/database-assets/`, with provenance recorded in that folder - unchanged current-wiki branding and homepage artwork under `static/wiki-assets/`, sourced from `Odjit/VRising-Mod-Wiki` on branch `explore/theme-revamp` - accepted visual review baselines under `tests/visual/baselines/` diff --git a/data/enrichment/asset-dump-lock.json b/data/enrichment/asset-dump-lock.json index b6f3e98906..0b791603ca 100644 --- a/data/enrichment/asset-dump-lock.json +++ b/data/enrichment/asset-dump-lock.json @@ -10,7 +10,7 @@ "texturePngCount": 12329, "stunlockIconPngCount": 2139, "textureAggregateSha256": "1D53084D1431B65D95FF5905ECC03D4BF8C36AD5392F69222E7AD3FB26B90734", - "materializedPublicIconCount": 780, + "materializedPublicIconCount": 830, "materializedIcons": { "public/icons/abilities/Stunlock_Icon_Ability_Spell_Blood_BloodFontain.png": "BF1F67BC06652D4E1B82EEB4E3597ED3E770D77636CC78CED20AD36E70FFA7AB", "public/icons/abilities/Stunlock_Icon_Ability_Spell_Blood_BloodRage.png": "D1BD9EA8448FF1FE75FE581ACB89D335E7A6E716543F9049563CA57A8F38D99F", @@ -791,6 +791,56 @@ "public/icons/items/Stunlock_Icon_Weapon_Sword_Unique_T08_Variation01.png": "0F62CFE0A6EC72C3A44555B6C088CAF855A10E6F3F59D4AB294514AC03496E33", "public/icons/items/Stunlock_Icon_Weapon_Twinblades_Unique_T08_Variation01.png": "5CC2D36DBA1233BFD10D1B8AD60E06281350B4E597F8C93DE8749176B94C4467", "public/icons/items/Stunlock_Icon_Whip_Unique_Variation01_Shattered.png": "BCF8106CF440543F3499086EBD859BFA0B0F496570D822748E97773087A6556B", - "public/icons/items/Stunlock_Icon_Whip_Unique_Variation01.png": "07695CAF0BED9CE85ADC61AED8FB6CDD7057781D0AF767D49490608F11BF1B1E" + "public/icons/items/Stunlock_Icon_Whip_Unique_Variation01.png": "07695CAF0BED9CE85ADC61AED8FB6CDD7057781D0AF767D49490608F11BF1B1E", + "public/icons/npcs/CHAR_ArchMage_VBlood_HeadPortrait.png": "08DAAA492AD2532EE0B526BEFC10FAC2F8139DDDA3EF19CD86BB74E5F373DA26", + "public/icons/npcs/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png": "5C1C15AF63EFEEC4489E14E58945E7609004DF2B2E8F206C0B25594EBD15E5ED", + "public/icons/npcs/CHAR_Bandit_Foreman_VBlood_HeadPortrait.png": "3EA4AA3D4D2D68437CBB6BFE9218CE78F403659ABF5BF8A93BDD8F2EB259B723", + "public/icons/npcs/CHAR_Bandit_Stalker_VBlood_HeadPortrait.png": "B603B52F7B773EA0C3E6AF5CB22D1037319D07DA068F09918E41BF1FFB8110B3", + "public/icons/npcs/CHAR_Bandit_Tourok_VBlood_HeadPortrait.png": "4C585D8B2E12AC30E3544C59A7C103AF44B3A667625103A93FAD9C6F538C9CDC", + "public/icons/npcs/CHAR_BatVampire_VBlood_HeadPortrait.png": "5AD6FFE4B48E8281551CAE596C7A049060B7BA38A48368D6094E075ABE529260", + "public/icons/npcs/CHAR_Militia_Guard_VBlood_HeadPortrait.png": "18B16F1CD17894C0A0F07337A419372B81AD80F9CF1D13C395AB399B415AAE33", + "public/icons/npcs/CHAR_Militia_Leader_VBlood_HeadPortrait.png": "3C778CA10412DC8C777C20650FCF82A86152D41DE0EC04E9310AF1E14997C2C1", + "public/icons/npcs/CHAR_Vermin_DireRat_VBlood_HeadPortrait.png": "71970D94D378AAE945F08D77F032C597F21DFE6DDE3BF7CD1B2CFF68EFE57940", + "public/icons/npcs/Portrait_Large_Normal_AlphaWolf.png": "AE1D98E48C0B72EB9ABD79C1FE5C8A09CF7CB7E022CF00BF987274527C196B0C", + "public/icons/npcs/Portrait_Large_Normal_ArenaChampion.png": "4FDD6FBAAEB8DB110BC7BA2F615FBDFC8DE3046E21DB75786351E926D42B2EDD", + "public/icons/npcs/Portrait_Large_Normal_AzarielSunbringer.png": "352D8295767C08929B352532566DC8FFF6324731D444B619607DF21092CB1F84", + "public/icons/npcs/Portrait_Large_Normal_BeatriceTailor.png": "049033227B1646D6A767CB985CEA29086F289AE0E5D5E57DB66DF2D9CAA69E01", + "public/icons/npcs/Portrait_Large_Normal_BloodCommander.png": "FB33348E40C5F9730285D58D4777EB4308ABAA7615C8278ABADB43E06027759E", + "public/icons/npcs/Portrait_Large_Normal_ChristinaSunPriestess.png": "CCCB82FF1D5416E4EB9FF409DE7D32E0EB7E41F29F15F482CF1DFC5DF95AAE2E", + "public/icons/npcs/Portrait_Large_Normal_CursedSmith.png": "5F9E5FBC5D46A7B01411AC684FC1F6630B7CF4323D2B7C5066F9F2633F9A356A", + "public/icons/npcs/Portrait_Large_Normal_CursedWanderer.png": "758907A728C5B64A357F89C97D22F4667514C13470A5E4B57E0EBAB6C879774C", + "public/icons/npcs/Portrait_Large_Normal_Dracula.png": "86360F13B15EC07D2599518EB1A6BF34CB5BDEAE5CC902894CBFFFAC1547E8B3", + "public/icons/npcs/Portrait_Large_Normal_DukeBalaton.png": "37902797970CDAD9CB80D1CBE79F177FB7F82BF4F34E5E5138F1BF4D76350061", + "public/icons/npcs/Portrait_Large_Normal_ErrolStonebreaker.png": "DC5243DA215E210A1A01C1B221CF79277BE813A750EFE327DCCF079A6551128A", + "public/icons/npcs/Portrait_Large_Normal_FerociousBear.png": "311DA62E8F94B6118D729BAE32607FD812B21A7831C1B834D2A6E922A4A990B9", + "public/icons/npcs/Portrait_Large_Normal_Fisherman.png": "0C06827ADAEA294F6CFBFD2EDBBAFB51291008C16B83B9C1CDA2C0C996E9B570", + "public/icons/npcs/Portrait_Large_Normal_ForgeBinder.png": "7367C0492739E7C4461D5AE0CBED906D454A85EE6CBA4D97DF8FA301191DDEF6", + "public/icons/npcs/Portrait_Large_Normal_FoulrotSoultaker.png": "39C8A4EF54A29F009BFC052A5FB881881E0079BB768DC11925EAAF346C1A8EE8", + "public/icons/npcs/Portrait_Large_Normal_FrostCommander.png": "667526AF4D028A57252FD0B2D992BBD0D4D5F4583606198AD87D5E1CB139CFAB", + "public/icons/npcs/Portrait_Large_Normal_FrostmawMountainTerror.png": "4F36145A249B2333AD6F4C7C986CDA4FE8645FAC4EF250186D546CF393341D9B", + "public/icons/npcs/Portrait_Large_Normal_Glassblower.png": "CB9E0D1C0235470FA75C47BC5DDF4683ADC31B0C70F346078D2D5F251BC12277", + "public/icons/npcs/Portrait_Large_Normal_GorecrusherBehemoth.png": "D3750FF72B62B1DE6530CF632DA767228801A643A8A5E68EC6213F756C11B76E", + "public/icons/npcs/Portrait_Large_Normal_GoreswineRavager.png": "9B4BDEE6F5553935F135AE1A13F3C37A7755D24F5A4C573FD5AA5200A80FADBB", + "public/icons/npcs/Portrait_Large_Normal_JadeVampireHunter.png": "B5A0CE78E1FD0C5FDCCCDDB0F5F03CC0139AD3F5BFC3F6D8A1BD3E29B1E2FC4F", + "public/icons/npcs/Portrait_Large_Normal_KeelyFrostArcher.png": "754FB41E1A649F0B0BD7946C23D7F25DEE2376B9522C40B90B6F8AE5A20496E0", + "public/icons/npcs/Portrait_Large_Normal_LeandraShadowPriestess.png": "5945BB69C7315D5E6F861740FDC8B8EF3784A6234254F52D0728A391AFEC86E5", + "public/icons/npcs/Portrait_Large_Normal_LidiaChaosArcher.png": "256EC8EE81B17310A57A104D593D5FFE685F19F65B23D58CA107785DE5E8819F", + "public/icons/npcs/Portrait_Large_Normal_MatkaCurseWeaver.png": "A13F8665CC6F851BB5E926D6EA7C54C065F712B9F8DA43F51B96DD2E36517D3D", + "public/icons/npcs/Portrait_Large_Normal_MeredithBrightArcher.png": "6F592A7E029AA4B39ED2F57889A59B8BD1DF095B4E601D8AA04CD42C20EBE48C", + "public/icons/npcs/Portrait_Large_Normal_MorianStormwingMatriarch.png": "CD53F2495417C396C11F14F468B6058DE7D47693866335A6657A3B79A6FCEAB4", + "public/icons/npcs/Portrait_Large_Normal_NicholausFallen.png": "B8C098C805E3634DE4B47A62D875811093E713CC0D417E047B46F1F46C763F45", + "public/icons/npcs/Portrait_Large_Normal_Overseer.png": "D25716970EC30CF7593551A1232EEC73464DDDCC1E8A9A35F72BC12DF25AECB5", + "public/icons/npcs/Portrait_Large_Normal_PoloraFeywalker.png": "A59ED0303AB3169A6D915F28F9ABA16A65DDC6C89410E0B9A95BAF3B1D8F21E9", + "public/icons/npcs/Portrait_Large_Normal_Professor.png": "A7151E9BA5A1D6EE28F42ED346FE7FB4BA1E8575DD7D69F9ECE3B995DF36AF21", + "public/icons/npcs/Portrait_Large_Normal_Purifier.png": "182EC3F71BFCA1786CAA5E59FBC147A7F0116648DE08671CCB49E02CCE8C39A8", + "public/icons/npcs/Portrait_Large_Normal_RazielShepherd.png": "E440EE3BA7F4BC2D2B2BBBDAC8AFE0E3FEDB80EE446674466C9A7AE9C1305844", + "public/icons/npcs/Portrait_Large_Normal_SolarusImmaculate.png": "67048A66614FF293C0138958AA91E9CD2E27B202CE700F42449D4E580067235F", + "public/icons/npcs/Portrait_Large_Normal_Sommelier.png": "0AE6AD617D4BE658808D61D59AE399798ADAD3459AD669F15BF7A5E5901F0FB5", + "public/icons/npcs/Portrait_Large_Normal_TerahGeomancer.png": "17E0699BAE5BC1B434F55AA8403CE819F3462CA30DE0CF6BBAB554C04044B960", + "public/icons/npcs/Portrait_Large_Normal_TerrorclawOgre.png": "1C3C3682AFB34D096F069A59CCEDF8A3DB4F8EDB13ECE4441FEFC5F47267428D", + "public/icons/npcs/Portrait_Large_Normal_TristanVampireHunter.png": "5E6A4FF249B0CACF35AA2B9CB51EFF1D3985990F2A9047910A58F617D2BA37EF", + "public/icons/npcs/Portrait_Large_Normal_UndeadGeneral.png": "44B630DAABA6843F04C2F4C6DA3FEDB22341210DC6AD2B4AD248B7DE9D35DE88", + "public/icons/npcs/Portrait_Large_Normal_UngoraSpiderQueen.png": "40CD8D2773FCD89E9C8D6669B6B629DB1373EFC61CEF543B4C01C53DCB007FAD", + "public/icons/npcs/Portrait_Large_Normal_WingedHorror.png": "CC7FA9EC52613BF0E466D55C2EA023046CE53ED3E121ADC866F2CBE12D595BA9" } } diff --git a/data/enrichment/npc-portrait-map.json b/data/enrichment/npc-portrait-map.json index 7a16f4173b..cc9fed8015 100644 --- a/data/enrichment/npc-portrait-map.json +++ b/data/enrichment/npc-portrait-map.json @@ -17,7 +17,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_ArchMage_VBlood_HeadPortrait.png" }, "CHAR_Bandit_Bomber_VBlood": { "prefab": "CHAR_Bandit_Bomber_VBlood", @@ -32,7 +33,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png" }, "CHAR_Bandit_Chaosarrow_VBlood": { "prefab": "CHAR_Bandit_Chaosarrow_VBlood", @@ -49,7 +51,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_LidiaChaosArcher.png" }, "CHAR_Bandit_Fisherman_VBlood": { "prefab": "CHAR_Bandit_Fisherman_VBlood", @@ -66,7 +69,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_Fisherman.png" }, "CHAR_Bandit_Foreman_VBlood": { "prefab": "CHAR_Bandit_Foreman_VBlood", @@ -81,7 +85,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_Bandit_Foreman_VBlood_HeadPortrait.png" }, "CHAR_Bandit_Frostarrow_VBlood": { "prefab": "CHAR_Bandit_Frostarrow_VBlood", @@ -98,7 +103,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_KeelyFrostArcher.png" }, "CHAR_Bandit_Stalker_VBlood": { "prefab": "CHAR_Bandit_Stalker_VBlood", @@ -113,7 +119,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_Bandit_Stalker_VBlood_HeadPortrait.png" }, "CHAR_Bandit_StoneBreaker_VBlood": { "prefab": "CHAR_Bandit_StoneBreaker_VBlood", @@ -130,7 +137,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_ErrolStonebreaker.png" }, "CHAR_Bandit_Tourok_VBlood": { "prefab": "CHAR_Bandit_Tourok_VBlood", @@ -145,7 +153,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_Bandit_Tourok_VBlood_HeadPortrait.png" }, "CHAR_BatVampire_VBlood": { "prefab": "CHAR_BatVampire_VBlood", @@ -160,7 +169,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_BatVampire_VBlood_HeadPortrait.png" }, "CHAR_Blackfang_Valyr_VBlood": { "prefab": "CHAR_Blackfang_Valyr_VBlood", @@ -177,7 +187,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_ForgeBinder.png" }, "CHAR_ChurchOfLight_Cardinal_VBlood": { "prefab": "CHAR_ChurchOfLight_Cardinal_VBlood", @@ -194,7 +205,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_AzarielSunbringer.png" }, "CHAR_ChurchOfLight_Overseer_VBlood": { "prefab": "CHAR_ChurchOfLight_Overseer_VBlood", @@ -211,7 +223,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_Overseer.png" }, "CHAR_ChurchOfLight_Paladin_VBlood": { "prefab": "CHAR_ChurchOfLight_Paladin_VBlood", @@ -228,7 +241,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_SolarusImmaculate.png" }, "CHAR_ChurchOfLight_Sommelier_VBlood": { "prefab": "CHAR_ChurchOfLight_Sommelier_VBlood", @@ -245,7 +259,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_Sommelier.png" }, "CHAR_Cursed_MountainBeast_VBlood": { "prefab": "CHAR_Cursed_MountainBeast_VBlood", @@ -262,7 +277,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_GorecrusherBehemoth.png" }, "CHAR_Cursed_ToadKing_VBlood": { "prefab": "CHAR_Cursed_ToadKing_VBlood", @@ -279,7 +295,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_DukeBalaton.png" }, "CHAR_Cursed_Witch_VBlood": { "prefab": "CHAR_Cursed_Witch_VBlood", @@ -296,7 +313,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_MatkaCurseWeaver.png" }, "CHAR_Forest_Bear_Dire_Vblood": { "prefab": "CHAR_Forest_Bear_Dire_Vblood", @@ -313,7 +331,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_FerociousBear.png" }, "CHAR_Forest_Wolf_VBlood": { "prefab": "CHAR_Forest_Wolf_VBlood", @@ -330,7 +349,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_AlphaWolf.png" }, "CHAR_Geomancer_Human_VBlood": { "prefab": "CHAR_Geomancer_Human_VBlood", @@ -347,7 +367,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_TerahGeomancer.png" }, "CHAR_Gloomrot_Purifier_VBlood": { "prefab": "CHAR_Gloomrot_Purifier_VBlood", @@ -364,7 +385,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_Purifier.png" }, "CHAR_Gloomrot_TheProfessor_VBlood": { "prefab": "CHAR_Gloomrot_TheProfessor_VBlood", @@ -381,7 +403,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_Professor.png" }, "CHAR_Harpy_Matriarch_VBlood": { "prefab": "CHAR_Harpy_Matriarch_VBlood", @@ -398,7 +421,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_MorianStormwingMatriarch.png" }, "CHAR_Manticore_VBlood": { "prefab": "CHAR_Manticore_VBlood", @@ -415,7 +439,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_WingedHorror.png" }, "CHAR_Militia_BishopOfDunley_VBlood": { "prefab": "CHAR_Militia_BishopOfDunley_VBlood", @@ -432,7 +457,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_RazielShepherd.png" }, "CHAR_Militia_Glassblower_VBlood": { "prefab": "CHAR_Militia_Glassblower_VBlood", @@ -449,7 +475,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_Glassblower.png" }, "CHAR_Militia_Guard_VBlood": { "prefab": "CHAR_Militia_Guard_VBlood", @@ -464,7 +491,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_Militia_Guard_VBlood_HeadPortrait.png" }, "CHAR_Militia_Leader_VBlood": { "prefab": "CHAR_Militia_Leader_VBlood", @@ -479,7 +507,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_Militia_Leader_VBlood_HeadPortrait.png" }, "CHAR_Militia_Longbowman_LightArrow_Vblood": { "prefab": "CHAR_Militia_Longbowman_LightArrow_Vblood", @@ -496,7 +525,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_MeredithBrightArcher.png" }, "CHAR_Militia_Nun_VBlood": { "prefab": "CHAR_Militia_Nun_VBlood", @@ -513,7 +543,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_ChristinaSunPriestess.png" }, "CHAR_Poloma_VBlood": { "prefab": "CHAR_Poloma_VBlood", @@ -530,7 +561,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_PoloraFeywalker.png" }, "CHAR_Spider_Queen_VBlood": { "prefab": "CHAR_Spider_Queen_VBlood", @@ -547,7 +579,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_UngoraSpiderQueen.png" }, "CHAR_Undead_ArenaChampion_VBlood": { "prefab": "CHAR_Undead_ArenaChampion_VBlood", @@ -564,7 +597,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_ArenaChampion.png" }, "CHAR_Undead_BishopOfDeath_VBlood": { "prefab": "CHAR_Undead_BishopOfDeath_VBlood", @@ -581,7 +615,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_GoreswineRavager.png" }, "CHAR_Undead_BishopOfShadows_VBlood": { "prefab": "CHAR_Undead_BishopOfShadows_VBlood", @@ -598,7 +633,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_LeandraShadowPriestess.png" }, "CHAR_Undead_CursedSmith_VBlood": { "prefab": "CHAR_Undead_CursedSmith_VBlood", @@ -615,7 +651,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_CursedSmith.png" }, "CHAR_Undead_Leader_Vblood": { "prefab": "CHAR_Undead_Leader_Vblood", @@ -632,7 +669,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_UndeadGeneral.png" }, "CHAR_Undead_Priest_VBlood": { "prefab": "CHAR_Undead_Priest_VBlood", @@ -649,7 +687,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_NicholausFallen.png" }, "CHAR_Undead_ZealousCultist_VBlood": { "prefab": "CHAR_Undead_ZealousCultist_VBlood", @@ -666,7 +705,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_FoulrotSoultaker.png" }, "CHAR_Vampire_BloodKnight_VBlood": { "prefab": "CHAR_Vampire_BloodKnight_VBlood", @@ -683,7 +723,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_BloodCommander.png" }, "CHAR_Vampire_Dracula_VBlood": { "prefab": "CHAR_Vampire_Dracula_VBlood", @@ -700,7 +741,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_Dracula.png" }, "CHAR_Vampire_IceRanger_VBlood": { "prefab": "CHAR_Vampire_IceRanger_VBlood", @@ -717,7 +759,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_FrostCommander.png" }, "CHAR_Vermin_DireRat_VBlood": { "prefab": "CHAR_Vermin_DireRat_VBlood", @@ -732,7 +775,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/All.json" - ] + ], + "portraitAssetPath": "/icons/npcs/CHAR_Vermin_DireRat_VBlood_HeadPortrait.png" }, "CHAR_VHunter_Jade_VBlood": { "prefab": "CHAR_VHunter_Jade_VBlood", @@ -749,7 +793,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_JadeVampireHunter.png" }, "CHAR_VHunter_Leader_VBlood": { "prefab": "CHAR_VHunter_Leader_VBlood", @@ -766,7 +811,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_TristanVampireHunter.png" }, "CHAR_Villager_CursedWanderer_VBlood": { "prefab": "CHAR_Villager_CursedWanderer_VBlood", @@ -783,7 +829,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_CursedWanderer.png" }, "CHAR_Villager_Tailor_VBlood": { "prefab": "CHAR_Villager_Tailor_VBlood", @@ -800,7 +847,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_BeatriceTailor.png" }, "CHAR_Wendigo_VBlood": { "prefab": "CHAR_Wendigo_VBlood", @@ -817,7 +865,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_FrostmawMountainTerror.png" }, "CHAR_Winter_Yeti_VBlood": { "prefab": "CHAR_Winter_Yeti_VBlood", @@ -834,7 +883,8 @@ "data/enrichment/npc-display-map.json", "data/enrichment/npc-classification-map.json", "data/prefabs/VBloodNames.json" - ] + ], + "portraitAssetPath": "/icons/npcs/Portrait_Large_Normal_TerrorclawOgre.png" } } } diff --git a/public/icons/npcs/CHAR_ArchMage_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_ArchMage_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..ea7b961a30 Binary files /dev/null and b/public/icons/npcs/CHAR_ArchMage_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..746b1cb466 Binary files /dev/null and b/public/icons/npcs/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/CHAR_Bandit_Foreman_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_Bandit_Foreman_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..8547cd102c Binary files /dev/null and b/public/icons/npcs/CHAR_Bandit_Foreman_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/CHAR_Bandit_Stalker_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_Bandit_Stalker_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..b6589ff0ad Binary files /dev/null and b/public/icons/npcs/CHAR_Bandit_Stalker_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/CHAR_Bandit_Tourok_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_Bandit_Tourok_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..a7a1cc0702 Binary files /dev/null and b/public/icons/npcs/CHAR_Bandit_Tourok_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/CHAR_BatVampire_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_BatVampire_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..1372e27141 Binary files /dev/null and b/public/icons/npcs/CHAR_BatVampire_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/CHAR_Militia_Guard_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_Militia_Guard_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..12ff70c292 Binary files /dev/null and b/public/icons/npcs/CHAR_Militia_Guard_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/CHAR_Militia_Leader_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_Militia_Leader_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..e6a884d2d2 Binary files /dev/null and b/public/icons/npcs/CHAR_Militia_Leader_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/CHAR_Vermin_DireRat_VBlood_HeadPortrait.png b/public/icons/npcs/CHAR_Vermin_DireRat_VBlood_HeadPortrait.png new file mode 100644 index 0000000000..90f7495b65 Binary files /dev/null and b/public/icons/npcs/CHAR_Vermin_DireRat_VBlood_HeadPortrait.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_AlphaWolf.png b/public/icons/npcs/Portrait_Large_Normal_AlphaWolf.png new file mode 100644 index 0000000000..caa2da501b Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_AlphaWolf.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_ArenaChampion.png b/public/icons/npcs/Portrait_Large_Normal_ArenaChampion.png new file mode 100644 index 0000000000..0bf9c5f050 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_ArenaChampion.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_AzarielSunbringer.png b/public/icons/npcs/Portrait_Large_Normal_AzarielSunbringer.png new file mode 100644 index 0000000000..3d749c1937 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_AzarielSunbringer.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_BeatriceTailor.png b/public/icons/npcs/Portrait_Large_Normal_BeatriceTailor.png new file mode 100644 index 0000000000..677b6f0d96 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_BeatriceTailor.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_BloodCommander.png b/public/icons/npcs/Portrait_Large_Normal_BloodCommander.png new file mode 100644 index 0000000000..80bdc5c7f2 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_BloodCommander.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_ChristinaSunPriestess.png b/public/icons/npcs/Portrait_Large_Normal_ChristinaSunPriestess.png new file mode 100644 index 0000000000..981113341f Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_ChristinaSunPriestess.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_CursedSmith.png b/public/icons/npcs/Portrait_Large_Normal_CursedSmith.png new file mode 100644 index 0000000000..f1dc46d88b Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_CursedSmith.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_CursedWanderer.png b/public/icons/npcs/Portrait_Large_Normal_CursedWanderer.png new file mode 100644 index 0000000000..14a9c01439 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_CursedWanderer.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_Dracula.png b/public/icons/npcs/Portrait_Large_Normal_Dracula.png new file mode 100644 index 0000000000..22d22893d7 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_Dracula.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_DukeBalaton.png b/public/icons/npcs/Portrait_Large_Normal_DukeBalaton.png new file mode 100644 index 0000000000..9e472aebe1 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_DukeBalaton.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_ErrolStonebreaker.png b/public/icons/npcs/Portrait_Large_Normal_ErrolStonebreaker.png new file mode 100644 index 0000000000..c2ffa36d3c Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_ErrolStonebreaker.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_FerociousBear.png b/public/icons/npcs/Portrait_Large_Normal_FerociousBear.png new file mode 100644 index 0000000000..3280b5f0b1 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_FerociousBear.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_Fisherman.png b/public/icons/npcs/Portrait_Large_Normal_Fisherman.png new file mode 100644 index 0000000000..b93b903e0a Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_Fisherman.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_ForgeBinder.png b/public/icons/npcs/Portrait_Large_Normal_ForgeBinder.png new file mode 100644 index 0000000000..7bf4ca9c21 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_ForgeBinder.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_FoulrotSoultaker.png b/public/icons/npcs/Portrait_Large_Normal_FoulrotSoultaker.png new file mode 100644 index 0000000000..8eaf00aa19 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_FoulrotSoultaker.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_FrostCommander.png b/public/icons/npcs/Portrait_Large_Normal_FrostCommander.png new file mode 100644 index 0000000000..4a81a8b556 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_FrostCommander.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_FrostmawMountainTerror.png b/public/icons/npcs/Portrait_Large_Normal_FrostmawMountainTerror.png new file mode 100644 index 0000000000..230c5bf33f Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_FrostmawMountainTerror.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_Glassblower.png b/public/icons/npcs/Portrait_Large_Normal_Glassblower.png new file mode 100644 index 0000000000..80b1fd23a1 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_Glassblower.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_GorecrusherBehemoth.png b/public/icons/npcs/Portrait_Large_Normal_GorecrusherBehemoth.png new file mode 100644 index 0000000000..b0250c8dbf Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_GorecrusherBehemoth.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_GoreswineRavager.png b/public/icons/npcs/Portrait_Large_Normal_GoreswineRavager.png new file mode 100644 index 0000000000..6619cd07ed Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_GoreswineRavager.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_JadeVampireHunter.png b/public/icons/npcs/Portrait_Large_Normal_JadeVampireHunter.png new file mode 100644 index 0000000000..5aef781f42 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_JadeVampireHunter.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_KeelyFrostArcher.png b/public/icons/npcs/Portrait_Large_Normal_KeelyFrostArcher.png new file mode 100644 index 0000000000..b30ff79d8a Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_KeelyFrostArcher.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_LeandraShadowPriestess.png b/public/icons/npcs/Portrait_Large_Normal_LeandraShadowPriestess.png new file mode 100644 index 0000000000..f53a2b315c Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_LeandraShadowPriestess.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_LidiaChaosArcher.png b/public/icons/npcs/Portrait_Large_Normal_LidiaChaosArcher.png new file mode 100644 index 0000000000..da16a0a523 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_LidiaChaosArcher.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_MatkaCurseWeaver.png b/public/icons/npcs/Portrait_Large_Normal_MatkaCurseWeaver.png new file mode 100644 index 0000000000..0e232d42ce Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_MatkaCurseWeaver.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_MeredithBrightArcher.png b/public/icons/npcs/Portrait_Large_Normal_MeredithBrightArcher.png new file mode 100644 index 0000000000..b4ace8ba0a Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_MeredithBrightArcher.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_MorianStormwingMatriarch.png b/public/icons/npcs/Portrait_Large_Normal_MorianStormwingMatriarch.png new file mode 100644 index 0000000000..c646299a7b Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_MorianStormwingMatriarch.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_NicholausFallen.png b/public/icons/npcs/Portrait_Large_Normal_NicholausFallen.png new file mode 100644 index 0000000000..112c38db06 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_NicholausFallen.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_Overseer.png b/public/icons/npcs/Portrait_Large_Normal_Overseer.png new file mode 100644 index 0000000000..bc6066420f Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_Overseer.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_PoloraFeywalker.png b/public/icons/npcs/Portrait_Large_Normal_PoloraFeywalker.png new file mode 100644 index 0000000000..f706c82fae Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_PoloraFeywalker.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_Professor.png b/public/icons/npcs/Portrait_Large_Normal_Professor.png new file mode 100644 index 0000000000..c08638eafb Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_Professor.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_Purifier.png b/public/icons/npcs/Portrait_Large_Normal_Purifier.png new file mode 100644 index 0000000000..06972e86de Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_Purifier.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_RazielShepherd.png b/public/icons/npcs/Portrait_Large_Normal_RazielShepherd.png new file mode 100644 index 0000000000..4b0a716b84 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_RazielShepherd.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_SolarusImmaculate.png b/public/icons/npcs/Portrait_Large_Normal_SolarusImmaculate.png new file mode 100644 index 0000000000..63d648fbf5 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_SolarusImmaculate.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_Sommelier.png b/public/icons/npcs/Portrait_Large_Normal_Sommelier.png new file mode 100644 index 0000000000..de6d66d7e2 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_Sommelier.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_TerahGeomancer.png b/public/icons/npcs/Portrait_Large_Normal_TerahGeomancer.png new file mode 100644 index 0000000000..c26ed630f7 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_TerahGeomancer.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_TerrorclawOgre.png b/public/icons/npcs/Portrait_Large_Normal_TerrorclawOgre.png new file mode 100644 index 0000000000..12f383f766 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_TerrorclawOgre.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_TristanVampireHunter.png b/public/icons/npcs/Portrait_Large_Normal_TristanVampireHunter.png new file mode 100644 index 0000000000..0f9d601f41 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_TristanVampireHunter.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_UndeadGeneral.png b/public/icons/npcs/Portrait_Large_Normal_UndeadGeneral.png new file mode 100644 index 0000000000..725ba03f3c Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_UndeadGeneral.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_UngoraSpiderQueen.png b/public/icons/npcs/Portrait_Large_Normal_UngoraSpiderQueen.png new file mode 100644 index 0000000000..47579a24f8 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_UngoraSpiderQueen.png differ diff --git a/public/icons/npcs/Portrait_Large_Normal_WingedHorror.png b/public/icons/npcs/Portrait_Large_Normal_WingedHorror.png new file mode 100644 index 0000000000..71f216ab24 Binary files /dev/null and b/public/icons/npcs/Portrait_Large_Normal_WingedHorror.png differ diff --git a/scripts/asset-dump-lock.ts b/scripts/asset-dump-lock.ts index 22818937b3..690adf9d5e 100644 --- a/scripts/asset-dump-lock.ts +++ b/scripts/asset-dump-lock.ts @@ -12,7 +12,8 @@ const expectedFolders = ["MonoBehaviour", "Sprite", "TextAsset", "Texture2D"]; const publicIconDirs = [ ["public/icons/abilities", "abilities"], ["public/icons/items", "items"], - ["public/icons/buildables", "buildables"] + ["public/icons/buildables", "buildables"], + ["public/icons/npcs", "npcs"] ] as const; interface TextureRecord { diff --git a/scripts/build-db-index.ts b/scripts/build-db-index.ts index 6f813117e4..98303facfa 100644 --- a/scripts/build-db-index.ts +++ b/scripts/build-db-index.ts @@ -372,6 +372,19 @@ interface BuildablePortraitMapSnapshot { entriesByPrefab?: Record; } +interface NpcPortraitMapEntry { + prefab: string; + guid: number; + portraitAssetName: string; + portraitAssetPath?: string; + joinStatus?: string; + approvalStatus?: string; +} + +interface NpcPortraitMapSnapshot { + entriesByPrefab?: Record; +} + interface NpcClassificationMapEntry { prefab: string; guid: number; @@ -399,6 +412,7 @@ interface BuildContext { recipeLinkByPrefab: Map; npcClassificationByPrefab: Map; npcDisplayByPrefab: Map; + npcPortraitByPrefab: Map; workstationDisplayByPrefab: Map; buildablePortraitByPrefab: Map; blueprintDisplayByPrefab: Map; @@ -1536,6 +1550,41 @@ function parseBuildablePortraitMap(snapshot: BuildablePortraitMapSnapshot | null ); } +function parseNpcPortraitMap(snapshot: NpcPortraitMapSnapshot | null): Map { + return new Map( + Object.entries(snapshot?.entriesByPrefab ?? {}) + .map(([prefabKey, rawEntry]) => { + if (!rawEntry || typeof rawEntry !== "object") { + return null; + } + + const entry = rawEntry as unknown as Record; + const prefab = toUnknownString(entry.prefab) ?? prefabKey; + const guid = toUnknownNumber(entry.guid); + const portraitAssetName = toUnknownString(entry.portraitAssetName); + const portraitAssetPath = toUnknownString(entry.portraitAssetPath); + const joinStatus = toUnknownString(entry.joinStatus); + const approvalStatus = toUnknownString(entry.approvalStatus); + if (!prefab || guid === undefined || !portraitAssetName) { + return null; + } + + return [ + prefab, + { + prefab, + guid, + portraitAssetName, + ...(portraitAssetPath ? { portraitAssetPath } : {}), + ...(joinStatus ? { joinStatus } : {}), + ...(approvalStatus ? { approvalStatus } : {}) + } + ] as const; + }) + .filter((entry): entry is readonly [string, NpcPortraitMapEntry] => Boolean(entry)) + ); +} + function parseNpcClassificationMap(snapshot: Record | null): Map { return new Map( Object.entries(snapshot ?? {}) @@ -1584,7 +1633,7 @@ function parseNpcClassificationMap(snapshot: Record | null): Ma async function loadBuildContext(repoRoot: string): Promise { const enrichmentDir = path.join(repoRoot, "data", "enrichment"); - const [localizedSnapshot, abilityCatalogSnapshot, abilityTooltipSnapshot, serverDamageEvidenceSnapshot, itemIconSnapshot, itemDescriptionSnapshot, recipeLinkSnapshot, npcClassificationSnapshot, npcDisplaySnapshot, workstationDisplaySnapshot, buildablePortraitSnapshot, blueprintDisplaySnapshot, questDisplaySnapshot, buffDisplaySnapshot, itemsetDisplaySnapshot] = + const [localizedSnapshot, abilityCatalogSnapshot, abilityTooltipSnapshot, serverDamageEvidenceSnapshot, itemIconSnapshot, itemDescriptionSnapshot, recipeLinkSnapshot, npcClassificationSnapshot, npcDisplaySnapshot, npcPortraitSnapshot, workstationDisplaySnapshot, buildablePortraitSnapshot, blueprintDisplaySnapshot, questDisplaySnapshot, buffDisplaySnapshot, itemsetDisplaySnapshot] = await Promise.all([ readJsonIfExists(path.join(enrichmentDir, "prefab-localization.json")), readJsonIfExists(path.join(enrichmentDir, "ability-catalog.json")), @@ -1595,6 +1644,7 @@ async function loadBuildContext(repoRoot: string): Promise { readJsonIfExists>(path.join(enrichmentDir, "recipe-link-map.json")), readJsonIfExists(path.join(enrichmentDir, "npc-classification-map.json")), readJsonIfExists>(path.join(enrichmentDir, "npc-display-map.json")), + readJsonIfExists(path.join(enrichmentDir, "npc-portrait-map.json")), readJsonIfExists>(path.join(enrichmentDir, "workstation-display-map.json")), readJsonIfExists(path.join(enrichmentDir, "buildable-portrait-map.json")), readJsonIfExists>(path.join(enrichmentDir, "blueprint-display-map.json")), @@ -1755,6 +1805,7 @@ async function loadBuildContext(repoRoot: string): Promise { recipeLinkByPrefab, npcClassificationByPrefab: parseNpcClassificationMap(npcClassificationSnapshot), npcDisplayByPrefab: parsePrefabDisplayMap(npcDisplaySnapshot), + npcPortraitByPrefab: parseNpcPortraitMap(npcPortraitSnapshot), workstationDisplayByPrefab: parsePrefabDisplayMap(workstationDisplaySnapshot), buildablePortraitByPrefab: parseBuildablePortraitMap(buildablePortraitSnapshot), blueprintDisplayByPrefab: parsePrefabDisplayMap(blueprintDisplaySnapshot), @@ -2069,6 +2120,14 @@ function buildNpcEntity(doc: PrefabDocument, components: Map { + const publicAssets = selectNpcPortraitPublicAssets( + { + schemaVersion: 1, + sourceKind: "provisional-vblood-portrait-map", + sourceRef: "data/enrichment/npc-portrait-candidates.json", + totalCurrentVbloodRows: 4, + entriesByPrefab: { + CHAR_Bandit_Bomber_VBlood: { + prefab: "CHAR_Bandit_Bomber_VBlood", + guid: 1896428751, + displayNameEn: "Clive the Firestarter", + portraitAssetName: "CHAR_Bandit_Bomber_VBlood_HeadPortrait.png", + portraitAssetFamily: "char-vblood-headportrait", + joinStatus: "source-backed", + evidenceRefs: [ + "Sprite/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png", + "Texture2D/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png", + "data/enrichment/npc-display-map.json" + ] + }, + CHAR_Bandit_Frostarrow_VBlood: { + prefab: "CHAR_Bandit_Frostarrow_VBlood", + guid: 1124739990, + displayNameEn: "Keely the Frost Archer", + portraitAssetName: "Portrait_Large_Normal_KeelyFrostArcher.png", + portraitAssetFamily: "portrait-large-normal", + joinStatus: "user-attested", + approvalStatus: "approved", + evidenceRefs: ["Sprite/Portrait_Large_Normal_KeelyFrostArcher.png", "data/enrichment/npc-display-map.json"] + } + } + }, + { + availableSourceRefs: [ + "Texture2D/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png", + "Sprite/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png", + "Sprite/Portrait_Large_Normal_KeelyFrostArcher.png" + ] + } + ); + + assert.deepEqual(publicAssets, [ + { + prefab: "CHAR_Bandit_Bomber_VBlood", + fileName: "CHAR_Bandit_Bomber_VBlood_HeadPortrait.png", + sourceRef: "Texture2D/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png", + publicPath: "/icons/npcs/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png" + }, + { + prefab: "CHAR_Bandit_Frostarrow_VBlood", + fileName: "Portrait_Large_Normal_KeelyFrostArcher.png", + sourceRef: "Sprite/Portrait_Large_Normal_KeelyFrostArcher.png", + publicPath: "/icons/npcs/Portrait_Large_Normal_KeelyFrostArcher.png" + } + ]); + + const withPaths = attachNpcPortraitAssetPaths( + { + schemaVersion: 1, + sourceKind: "provisional-vblood-portrait-map", + sourceRef: "data/enrichment/npc-portrait-candidates.json", + totalCurrentVbloodRows: 4, + entriesByPrefab: { + CHAR_Bandit_Bomber_VBlood: { + prefab: "CHAR_Bandit_Bomber_VBlood", + guid: 1896428751, + displayNameEn: "Clive the Firestarter", + portraitAssetName: "CHAR_Bandit_Bomber_VBlood_HeadPortrait.png", + portraitAssetFamily: "char-vblood-headportrait", + joinStatus: "source-backed", + evidenceRefs: ["Texture2D/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png"] + } + } + }, + publicAssets + ); + + assert.equal( + withPaths.entriesByPrefab.CHAR_Bandit_Bomber_VBlood.portraitAssetPath, + "/icons/npcs/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png" + ); +}); + +test("selectNpcPortraitPublicAssets skips unapproved and unavailable portrait rows", () => { + const publicAssets = selectNpcPortraitPublicAssets( + { + schemaVersion: 1, + sourceKind: "provisional-vblood-portrait-map", + sourceRef: "data/enrichment/npc-portrait-candidates.json", + totalCurrentVbloodRows: 4, + entriesByPrefab: { + pending: { + prefab: "pending", + guid: 1, + displayNameEn: "Pending", + portraitAssetName: "Portrait_Large_Normal_Pending.png", + portraitAssetFamily: "portrait-large-normal", + joinStatus: "user-attested", + approvalStatus: "pending", + evidenceRefs: ["Texture2D/Portrait_Large_Normal_Pending.png"] + }, + circumstantial: { + prefab: "circumstantial", + guid: 2, + displayNameEn: "Circumstantial", + portraitAssetName: "Portrait_Large_Normal_Circumstantial.png", + portraitAssetFamily: "portrait-large-normal", + joinStatus: "circumstantial", + approvalStatus: "pending", + evidenceRefs: ["Texture2D/Portrait_Large_Normal_Circumstantial.png"] + }, + unsafe: { + prefab: "unsafe", + guid: 3, + displayNameEn: "Unsafe", + portraitAssetName: "Portrait_Large_Normal_Unsafe.png", + portraitAssetFamily: "portrait-large-normal", + joinStatus: "unsafe", + approvalStatus: "rejected", + evidenceRefs: ["Texture2D/Portrait_Large_Normal_Unsafe.png"] + }, + missingSource: { + prefab: "missingSource", + guid: 4, + displayNameEn: "Missing Source", + portraitAssetName: "Portrait_Large_Normal_MissingSource.png", + portraitAssetFamily: "portrait-large-normal", + joinStatus: "source-backed", + evidenceRefs: ["Texture2D/Portrait_Large_Normal_MissingSource.png"] + } + } as any + }, + { availableSourceRefs: ["Texture2D/Portrait_Large_Normal_Pending.png"] } + ); + + assert.deepEqual(publicAssets, []); +}); + async function main() { for (const { name, run } of tests) { await run(); diff --git a/scripts/npc-portraits.ts b/scripts/npc-portraits.ts index e92b1ab4f6..20e1aa88d9 100644 --- a/scripts/npc-portraits.ts +++ b/scripts/npc-portraits.ts @@ -36,6 +36,7 @@ export interface NpcPortraitMapEntry { displayNameEn: string; portraitAssetName: string; portraitAssetFamily: string; + portraitAssetPath?: string; joinStatus: Extract; approvalStatus?: Extract; approvalNote?: string; @@ -59,6 +60,18 @@ export interface NpcPortraitBuildOptions { vbloodNamesRows: Array<[string, string, string]>; } +export interface NpcPortraitPublicAsset { + prefab: string; + fileName: string; + sourceRef: string; + publicPath: string; +} + +export interface SelectNpcPortraitPublicAssetOptions { + maxPublicAssets?: number; + availableSourceRefs?: Iterable; +} + type AssetRecord = { assetName: string; assetFamily: string; @@ -303,6 +316,84 @@ function sortSourceRefs(sourceRefs: string[]): string[] { return [...sourceRefs].sort((left, right) => rank(left) - rank(right) || left.localeCompare(right)); } +function preferredPublicSourceRef(entry: NpcPortraitMapEntry, availableSourceRefs?: Set): string | undefined { + const textureRef = `Texture2D/${entry.portraitAssetName}`; + const spriteRef = `Sprite/${entry.portraitAssetName}`; + for (const sourceRef of [textureRef, spriteRef]) { + if (!entry.evidenceRefs.includes(sourceRef)) { + continue; + } + if (availableSourceRefs && !availableSourceRefs.has(sourceRef)) { + continue; + } + return sourceRef; + } + return undefined; +} + +function isPromotedPortraitEntry(entry: NpcPortraitMapEntry): boolean { + return entry.joinStatus === "source-backed" || (entry.joinStatus === "user-attested" && entry.approvalStatus === "approved"); +} + +export function selectNpcPortraitPublicAssets( + portraitMap: NpcPortraitMapSnapshot, + options: SelectNpcPortraitPublicAssetOptions = {} +): NpcPortraitPublicAsset[] { + const availableSourceRefs = options.availableSourceRefs ? new Set(options.availableSourceRefs) : undefined; + const maxPublicAssets = options.maxPublicAssets ?? 75; + const assets: NpcPortraitPublicAsset[] = []; + + for (const entry of Object.values(portraitMap.entriesByPrefab)) { + if (!isPromotedPortraitEntry(entry)) { + continue; + } + const sourceRef = preferredPublicSourceRef(entry, availableSourceRefs); + if (!sourceRef) { + continue; + } + assets.push({ + prefab: entry.prefab, + fileName: entry.portraitAssetName, + sourceRef, + publicPath: `/icons/npcs/${entry.portraitAssetName}` + }); + } + + const uniqueAssets = new Map(); + for (const asset of assets.sort((left, right) => left.prefab.localeCompare(right.prefab) || left.fileName.localeCompare(right.fileName))) { + uniqueAssets.set(asset.prefab, asset); + } + + if (uniqueAssets.size > maxPublicAssets) { + throw new Error(`Refusing to materialize ${uniqueAssets.size} NPC portrait assets; expected at most ${maxPublicAssets}.`); + } + + return [...uniqueAssets.values()]; +} + +export function attachNpcPortraitAssetPaths( + portraitMap: NpcPortraitMapSnapshot, + publicAssets: NpcPortraitPublicAsset[] +): NpcPortraitMapSnapshot { + const publicPathByPrefab = new Map(publicAssets.map((asset) => [asset.prefab, asset.publicPath])); + return { + ...portraitMap, + entriesByPrefab: Object.fromEntries( + Object.entries(portraitMap.entriesByPrefab).map(([prefab, entry]) => { + const entryWithoutPath = { ...entry }; + delete entryWithoutPath.portraitAssetPath; + return [ + prefab, + { + ...entryWithoutPath, + ...(publicPathByPrefab.has(prefab) ? { portraitAssetPath: publicPathByPrefab.get(prefab) } : {}) + } + ]; + }) + ) + }; +} + function assetFamily(assetName: string): string | undefined { if (/^CHAR_.*_VBlood_HeadPortrait\.png$/i.test(assetName)) { return "char-vblood-headportrait"; diff --git a/scripts/refresh-db-assets.ts b/scripts/refresh-db-assets.ts index 5fbd608d8f..200e85b715 100644 --- a/scripts/refresh-db-assets.ts +++ b/scripts/refresh-db-assets.ts @@ -5,7 +5,7 @@ import { assertAssetDumpLock, syncAssetRefDirectory, syncIconDirectory } from ". import { resolveAssetDumpDir } from "./asset-dump-resolver"; import { attachBuildablePortraitAssetPaths, buildBuildablePortraitSnapshots, selectBuildablePortraitPublicAssets } from "./buildable-portraits"; import { buildBloodHuntsMapSnapshot, bloodHuntsSourceKind, type BloodHuntsMapSnapshot } from "./blood-hunts"; -import { buildNpcPortraitSnapshots } from "./npc-portraits"; +import { attachNpcPortraitAssetPaths, buildNpcPortraitSnapshots, selectNpcPortraitPublicAssets } from "./npc-portraits"; import { isNpcDisplayCandidateDoc } from "./npc-display-classification"; import { extractTextVariables, @@ -2781,6 +2781,7 @@ async function main() { const publicAbilityIconsDir = path.join(repoRoot, "public", "icons", "abilities"); const publicItemIconsDir = path.join(repoRoot, "public", "icons", "items"); const publicBuildableIconsDir = path.join(repoRoot, "public", "icons", "buildables"); + const publicNpcIconsDir = path.join(repoRoot, "public", "icons", "npcs"); const bloodHuntsSourcePath = path.join(assetDumpDir, "MonoBehaviour", "BloodHuntsDataAuthoring.json"); await Promise.all([ @@ -3456,7 +3457,7 @@ async function main() { localizedTextSourceRef: "Resources/Localization/English.json:Nodes", npcDisplaySourceRef: "data/enrichment/npc-display-map.json" }); - const stableNpcPortraitSnapshots = await buildNpcPortraitSnapshots({ + const rawNpcPortraitSnapshots = await buildNpcPortraitSnapshots({ assetDumpDir, allPrefabs, npcDisplayByPrefab: displaySnapshotsByDomain.get("npc") ?? {}, @@ -3464,6 +3465,16 @@ async function main() { bloodHuntsByGuid: stableBloodHuntsSnapshot.entriesByGuid, vbloodNamesRows: vBloodNamesRows }); + const npcPortraitPublicAssets = selectNpcPortraitPublicAssets(rawNpcPortraitSnapshots.portraitMap); + const npcPortraitSync = await syncAssetRefDirectory({ + assetDumpDir, + targetDir: publicNpcIconsDir, + files: npcPortraitPublicAssets + }); + const stableNpcPortraitSnapshots = { + candidates: rawNpcPortraitSnapshots.candidates, + portraitMap: attachNpcPortraitAssetPaths(rawNpcPortraitSnapshots.portraitMap, npcPortraitPublicAssets) + }; const rawBuildablePortraitSnapshots = await buildBuildablePortraitSnapshots({ assetDumpDir, allPrefabs, @@ -3608,6 +3619,9 @@ async function main() { console.log( `Materialized ${buildablePortraitPublicAssets.length} repo-owned buildable portrait assets (${buildablePortraitSync.copied} copied, ${buildablePortraitSync.unchanged} unchanged, ${buildablePortraitSync.deleted} deleted).` ); + console.log( + `Materialized ${npcPortraitPublicAssets.length} repo-owned NPC portrait assets (${npcPortraitSync.copied} copied, ${npcPortraitSync.unchanged} unchanged, ${npcPortraitSync.deleted} deleted).` + ); for (const domain of displayDomains) { console.log(`Imported ${importedLegacyDisplayRowsByDomain[domain.domainName] ?? 0} legacy ${domain.domainName} display rows.`); } diff --git a/scripts/validate-generated-data.ts b/scripts/validate-generated-data.ts index cd12023d6a..6a7b2c4c36 100644 --- a/scripts/validate-generated-data.ts +++ b/scripts/validate-generated-data.ts @@ -137,6 +137,7 @@ type NpcPortraitMapEntry = { displayNameEn: string; portraitAssetName: string; portraitAssetFamily: string; + portraitAssetPath?: string; joinStatus: string; approvalStatus?: string; approvalNote?: string; @@ -480,6 +481,16 @@ async function validateNpcPortraitMaps(repoRoot: string): Promise { assert(candidate?.candidateGuid === entry.guid, `${source}: candidate guid does not match promoted guid`); assert(candidate?.joinStatus === entry.joinStatus, `${source}: candidate joinStatus does not match promoted joinStatus`); assert(Array.isArray(entry.evidenceRefs) && entry.evidenceRefs.length > 0, `${source}: missing evidenceRefs`); + if (entry.portraitAssetPath) { + assert(entry.joinStatus === "source-backed" || entry.approvalStatus === "approved", `${source}: portraitAssetPath requires source-backed or approved user-attested evidence`); + assertNpcPortraitPath(entry.portraitAssetPath, source); + assert(path.posix.basename(entry.portraitAssetPath) === entry.portraitAssetName, `${source}: portraitAssetPath basename must match portraitAssetName`); + assert( + entry.evidenceRefs.includes(`Texture2D/${entry.portraitAssetName}`) || entry.evidenceRefs.includes(`Sprite/${entry.portraitAssetName}`), + `${source}: portraitAssetPath must be backed by a Texture2D or Sprite evidence ref` + ); + await assertPublicIconExists(repoRoot, entry.portraitAssetPath, source); + } } } @@ -650,6 +661,16 @@ function assertBuildablePortraitPath(icon: string | undefined, source: string): assert(!icon.includes(".."), `${source}: icon '${icon}' must not contain parent traversal`); } +function assertNpcPortraitPath(icon: string | undefined, source: string): void { + if (!icon) { + return; + } + + assert(icon.startsWith("/icons/npcs/"), `${source}: icon '${icon}' is not an approved NPC portrait path`); + assert(icon.endsWith(".png"), `${source}: icon '${icon}' must be a PNG asset`); + assert(!icon.includes(".."), `${source}: icon '${icon}' must not contain parent traversal`); +} + function collectRelatedIcons(detail: DetailEntry): Array<[string, string]> { const relationGroups: RelatedEntityGroupKey[] = ["repairRecipes", "relatedRecipes", "outputs", "requirements", "repairCosts", "spellJewels", "workstationOutputs", "inventoryPrefabs"]; @@ -750,6 +771,10 @@ async function main() { assertBuildablePortraitPath(detail.portraitAssetPath, `${filePath}:${detail.slug}.portraitAssetPath`); await assertPublicIconExists(repoRoot, detail.portraitAssetPath, `${filePath}:${detail.slug}.portraitAssetPath`); } + if (section === "npcs" && detail.portraitAssetPath) { + assertNpcPortraitPath(detail.portraitAssetPath, `${filePath}:${detail.slug}.portraitAssetPath`); + await assertPublicIconExists(repoRoot, detail.portraitAssetPath, `${filePath}:${detail.slug}.portraitAssetPath`); + } } } diff --git a/src/components/db/DbDetailView.test.tsx b/src/components/db/DbDetailView.test.tsx index 79cf9335c8..2b0d0ce054 100644 --- a/src/components/db/DbDetailView.test.tsx +++ b/src/components/db/DbDetailView.test.tsx @@ -152,6 +152,25 @@ test("structured NPC detail keeps summary cues while consolidating duplicate rel assert.equal(countMatches(html, /href="#relation-essence-drops"/g), 0); }); +test("structured NPC detail renders a portrait only when a source-backed path exists", () => { + const html = renderDetail( + { + ...npcDetailFixture, + title: "Clive the Firestarter", + npcKind: "V Blood Boss", + isVBlood: true, + portraitAssetPath: "/icons/npcs/CHAR_Bandit_Bomber_VBlood_HeadPortrait.png" + }, + "npcs" + ); + const withoutPath = renderNpcDetail(); + + assert.match(html, /src="\/icons\/npcs\/CHAR_Bandit_Bomber_VBlood_HeadPortrait\.png"/); + assert.match(html, /alt="Clive the Firestarter NPC portrait"/); + assert.doesNotMatch(withoutPath, /NPC portrait/); + assert.doesNotMatch(withoutPath, /\/icons\/npcs\//); +}); + test("structured workstation detail keeps summary cues while consolidating duplicate relation sections", () => { const html = renderWorkstationDetail(); diff --git a/src/components/db/DbDetailView.tsx b/src/components/db/DbDetailView.tsx index 757d5d7bab..164cb29a67 100644 --- a/src/components/db/DbDetailView.tsx +++ b/src/components/db/DbDetailView.tsx @@ -1467,6 +1467,24 @@ function renderWorkstationTitlePortrait(section: DbSection, detail: DbEntityDeta ); } +function renderNpcTitlePortrait(section: DbSection, detail: DbEntityDetail) { + const portraitAssetPath = typeof detail.portraitAssetPath === "string" ? detail.portraitAssetPath : undefined; + if (section !== "npcs" || !portraitAssetPath) { + return null; + } + + return ( + + {`${detail.title} + + ); +} + function renderItemTitleIcon(section: DbSection, detail: DbEntityDetail) { const icon = typeof detail.icon === "string" ? detail.icon : undefined; if (section !== "items" || !icon) { @@ -1562,6 +1580,7 @@ function renderHero(section: DbSection, detail: DbEntityDetail, factRows: DbDisp

{detail.title}

{renderWorkstationTitlePortrait(section, detail)} + {renderNpcTitlePortrait(section, detail)} {renderItemTitleIcon(section, detail)}
{subtitle ?

{subtitle}

: null}