diff --git a/assets/img/alliance.png b/assets/img/alliance.png new file mode 100644 index 0000000000..34d97a35bc Binary files /dev/null and b/assets/img/alliance.png differ diff --git a/assets/img/horde.png b/assets/img/horde.png new file mode 100644 index 0000000000..f4c4fbea00 Binary files /dev/null and b/assets/img/horde.png differ diff --git a/proto/ui.proto b/proto/ui.proto index 343ba90593..3ebab6e9ee 100644 --- a/proto/ui.proto +++ b/proto/ui.proto @@ -104,12 +104,31 @@ enum DungeonDifficulty { DifficultyRaid25H = 6; } +enum RepLevel { + RepLevelUnknown = 0; + RepLevelHated = 1; + RepLevelHostile = 2; + RepLevelUnfriendly = 3; + RepLevelNeutral = 4; + RepLevelFriendly = 5; + RepLevelHonored = 6; + RepLevelRevered = 7; + RepLevelExalted = 8; +} + +// TODO: Wotlk Rep Factions +// Use the faction ID for the field index +enum RepFaction { + RepFactionUnknown = 0; +} + message UIItemSource { oneof source { CraftedSource crafted = 1; DropSource drop = 2; QuestSource quest = 3; SoldBySource sold_by = 4; + RepSource rep = 5; } } message CraftedSource { @@ -132,6 +151,11 @@ message SoldBySource { string npc_name = 2; int32 zone_id = 3; } +message RepSource { + RepFaction rep_faction_id = 1; + RepLevel rep_level = 2; + Faction faction_id = 3; +} message UIEnchant { // All enchants have an effect ID. Some also have an item ID, others have a spell ID, @@ -142,7 +166,7 @@ message UIEnchant { int32 item_id = 2; // ID of the enchant "item". Might be 0 if not available. int32 spell_id = 3; // ID of the enchant "spell". Might be 0 if not available. - string name = 4; + string name = 4; string icon = 5; ItemType type = 6; // Which type of item this enchant can be applied to. diff --git a/ui/core/components/gear_picker.tsx b/ui/core/components/gear_picker.tsx index 7674bae0f4..f3f245c360 100644 --- a/ui/core/components/gear_picker.tsx +++ b/ui/core/components/gear_picker.tsx @@ -6,12 +6,12 @@ import { setItemQualityCssClass } from '../css_utils'; import { IndividualSimUI } from '../individual_sim_ui.js'; import { Player } from '../player'; import { Class, GemColor, ItemQuality, ItemSlot, ItemSpec, ItemType } from '../proto/common'; -import { DatabaseFilters, UIEnchant as Enchant, UIGem as Gem, UIItem as Item } from '../proto/ui.js'; +import { DatabaseFilters, RepFaction, UIEnchant as Enchant, UIGem as Gem, UIItem as Item, UIItem_FactionRestriction } from '../proto/ui.js'; import { ActionId } from '../proto_utils/action_id'; import { getEnchantDescription, getUniqueEnchantString } from '../proto_utils/enchants'; import { EquippedItem } from '../proto_utils/equipped_item'; import { gemMatchesSocket, getEmptyGemSocketIconUrl } from '../proto_utils/gems'; -import { difficultyNames, professionNames, slotNames } from '../proto_utils/names.js'; +import { difficultyNames, professionNames, REP_FACTION_NAMES, REP_LEVEL_NAMES, slotNames } from '../proto_utils/names.js'; import { Stats } from '../proto_utils/stats'; import { Sim } from '../sim.js'; import { SimUI } from '../sim_ui'; @@ -694,7 +694,7 @@ export class SelectorModal extends BaseModal { ilist.dispose(); }); - tabAnchor.value!.addEventListener('shown.bs.tab', event => { + tabAnchor.value!.addEventListener('shown.bs.tab', _event => { ilist.sizeRefresh(); }); @@ -840,14 +840,8 @@ export class ItemList { title: EP_TOOLTIP, }); - const show1hWeaponsSelector = makeShow1hWeaponsSelector( - this.tabContent.getElementsByClassName('selector-modal-show-1h-weapons')[0] as HTMLElement, - player.sim, - ); - const show2hWeaponsSelector = makeShow2hWeaponsSelector( - this.tabContent.getElementsByClassName('selector-modal-show-2h-weapons')[0] as HTMLElement, - player.sim, - ); + makeShow1hWeaponsSelector(this.tabContent.getElementsByClassName('selector-modal-show-1h-weapons')[0] as HTMLElement, player.sim); + makeShow2hWeaponsSelector(this.tabContent.getElementsByClassName('selector-modal-show-2h-weapons')[0] as HTMLElement, player.sim); if (!(label == 'Items' && (slot == ItemSlot.ItemSlotMainHand || (slot == ItemSlot.ItemSlotOffHand && player.getClass() == Class.ClassWarrior)))) { (this.tabContent.getElementsByClassName('selector-modal-show-1h-weapons')[0] as HTMLElement).style.display = 'none'; (this.tabContent.getElementsByClassName('selector-modal-show-2h-weapons')[0] as HTMLElement).style.display = 'none'; @@ -855,15 +849,12 @@ export class ItemList { makeShowEPValuesSelector(this.tabContent.getElementsByClassName('selector-modal-show-ep-values')[0] as HTMLElement, player.sim); - const showMatchingGemsSelector = makeShowMatchingGemsSelector( - this.tabContent.getElementsByClassName('selector-modal-show-matching-gems')[0] as HTMLElement, - player.sim, - ); + makeShowMatchingGemsSelector(this.tabContent.getElementsByClassName('selector-modal-show-matching-gems')[0] as HTMLElement, player.sim); if (!label.startsWith('Gem')) { (this.tabContent.getElementsByClassName('selector-modal-show-matching-gems')[0] as HTMLElement).style.display = 'none'; } - const phaseSelector = makePhaseSelector(this.tabContent.getElementsByClassName('selector-modal-phase-selector')[0] as HTMLElement, player.sim); + makePhaseSelector(this.tabContent.getElementsByClassName('selector-modal-phase-selector')[0] as HTMLElement, player.sim); if (label == 'Items') { const filtersButton = this.tabContent.getElementsByClassName('selector-modal-filters-button')[0] as HTMLElement; @@ -907,7 +898,7 @@ export class ItemList { ); const removeButton = this.tabContent.getElementsByClassName('selector-modal-remove-button')[0] as HTMLButtonElement; - removeButton.addEventListener('click', event => { + removeButton.addEventListener('click', _event => { onRemove(TypedEvent.nextEventID()); }); @@ -928,7 +919,7 @@ export class ItemList { player.sim.showExperimentalChangeEmitter.on(() => { simAllButton.hidden = !player.sim.getShowExperimental(); }); - simAllButton.addEventListener('click', event => { + simAllButton.addEventListener('click', _event => { if (simUI instanceof IndividualSimUI) { const itemSpecs = Array(); const isRangedOrTrinket = @@ -1222,22 +1213,26 @@ export class ItemList { } private getSourceInfo(item: Item, sim: Sim): JSX.Element { - if (!item.sources || item.sources.length == 0) { - return <>; - } - - const makeAnchor = (href: string, inner: string) => { + const makeAnchor = (href: string, inner: string | JSX.Element) => { return ( - + {inner} ); }; - const source = item.sources[0]; + if (!item.sources || item.sources.length == 0) { + return <>; + } + + let source = item.sources[0]; if (source.source.oneofKind == 'crafted') { const src = source.source.crafted; - return makeAnchor(ActionId.makeSpellUrl(src.spellId), professionNames.get(src.profession) ?? 'Unknown'); + + if (src.spellId) { + return makeAnchor(ActionId.makeSpellUrl(src.spellId), professionNames.get(src.profession) ?? 'Unknown'); + } + return makeAnchor(ActionId.makeItemUrl(item.id), professionNames.get(src.profession) ?? 'Unknown'); } else if (source.source.oneofKind == 'drop') { const src = source.source.drop; const zone = sim.db.getZone(src.zoneId); @@ -1246,30 +1241,78 @@ export class ItemList { throw new Error('No zone found for item: ' + item); } - const rtnEl = makeAnchor(ActionId.makeZoneUrl(zone.id), `${zone.name} (${difficultyNames.get(src.difficulty) ?? 'Unknown'})`); - const category = src.category ? ` - ${src.category}` : ''; if (npc) { - rtnEl.appendChild(document.createElement('br')); - rtnEl.appendChild(makeAnchor(ActionId.makeNpcUrl(npc.id), `${npc.name + category}`)); + return makeAnchor( + ActionId.makeNpcUrl(npc.id), + + {zone.name} ({difficultyNames.get(src.difficulty) ?? 'Unknown'}) +
+ {npc.name + category} +
, + ); } else if (src.otherName) { - /*innerHTML += ` -
- ${src.otherName + category} - `;*/ - } else if (category) { - /*innerHTML += ` -
- ${category} - `;*/ + return makeAnchor( + ActionId.makeZoneUrl(zone.id), + + {zone.name} +
+ {src.otherName} +
, + ); } - return rtnEl; - } else if (source.source.oneofKind == 'quest') { + return makeAnchor(ActionId.makeZoneUrl(zone.id), zone.name); + } else if (source.source.oneofKind == 'quest' && source.source.quest.name) { const src = source.source.quest; - return makeAnchor(ActionId.makeQuestUrl(src.id), src.name); + return makeAnchor( + ActionId.makeQuestUrl(src.id), + + Quest + {item.factionRestriction == UIItem_FactionRestriction.ALLIANCE_ONLY && ( + + )} + {item.factionRestriction == UIItem_FactionRestriction.HORDE_ONLY && ( + + )} +
+ {src.name} +
, + ); + } else if ((source = item.sources.find(source => source.source.oneofKind == 'rep') ?? source).source.oneofKind == 'rep') { + const factionNames = item.sources + .filter(source => source.source.oneofKind == 'rep') + .map(source => + source.source.oneofKind == 'rep' ? REP_FACTION_NAMES[source.source.rep.repFactionId] : REP_FACTION_NAMES[RepFaction.RepFactionUnknown], + ); + const src = source.source.rep; + return makeAnchor( + ActionId.makeItemUrl(item.id), + <> + {factionNames.map(name => ( + + {name} + {item.factionRestriction == UIItem_FactionRestriction.ALLIANCE_ONLY && ( + + )} + {item.factionRestriction == UIItem_FactionRestriction.HORDE_ONLY && ( + + )} +
+
+ ))} + {REP_LEVEL_NAMES[src.repLevel]} + , + ); } else if (source.source.oneofKind == 'soldBy') { const src = source.source.soldBy; - return makeAnchor(ActionId.makeNpcUrl(src.npcId), src.npcName); + return makeAnchor( + ActionId.makeNpcUrl(src.npcId), + + Sold by +
+ {src.npcName} +
, + ); } return <>; } diff --git a/ui/core/proto_utils/names.ts b/ui/core/proto_utils/names.ts index 1c1889da5e..f68df8506c 100644 --- a/ui/core/proto_utils/names.ts +++ b/ui/core/proto_utils/names.ts @@ -1,20 +1,6 @@ -import { - ArmorType, - Class, - ItemSlot, - Profession, - PseudoStat, - Race, - RangedWeaponType, - Stat, - WeaponType, -} from '../proto/common.js'; -import { - DungeonDifficulty, - RaidFilterOption, - SourceFilterOption, -} from '../proto/ui.js'; import { ResourceType } from '../proto/api.js'; +import { ArmorType, Class, ItemSlot, Profession, PseudoStat, Race, RangedWeaponType, Stat, WeaponType } from '../proto/common.js'; +import { DungeonDifficulty, RaidFilterOption, RepFaction, RepLevel, SourceFilterOption } from '../proto/ui.js'; export const armorTypeNames: Map = new Map([ [ArmorType.ArmorTypeUnknown, 'Unknown'], @@ -117,7 +103,7 @@ export function nameToProfession(name: string): Profession { const lower = name.toLowerCase(); for (const [key, value] of professionNames) { if (value.toLowerCase() == lower) { - return key + return key; } } return Profession.ProfessionUnknown; @@ -226,8 +212,7 @@ export const pseudoStatNames: Map = new Map([ export function getClassStatName(stat: Stat, playerClass: Class): string { const statName = statNames.get(stat); - if (!statName) - return 'UnknownStat'; + if (!statName) return 'UnknownStat'; if (playerClass == Class.ClassHunter) { return statName.replace('Melee', 'Ranged'); } else { @@ -317,7 +302,7 @@ export const raidNames: Map = new Map([ [RaidFilterOption.RaidVaultOfArchavon, 'Vault of Archavon'], [RaidFilterOption.RaidUlduar, 'Ulduar'], [RaidFilterOption.RaidTrialOfTheCrusader, 'Trial of the Crusader'], - [RaidFilterOption.RaidOnyxiasLair, 'Onyxia\'s Lair'], + [RaidFilterOption.RaidOnyxiasLair, "Onyxia's Lair"], [RaidFilterOption.RaidIcecrownCitadel, 'Icecrown Citadel'], [RaidFilterOption.RaidRubySanctum, 'Ruby Sanctum'], ]); @@ -333,3 +318,19 @@ export const difficultyNames: Map = new Map([ [DungeonDifficulty.DifficultyRaid25, '25N'], [DungeonDifficulty.DifficultyRaid25H, '25H'], ]); + +export const REP_LEVEL_NAMES: Record = { + [RepLevel.RepLevelUnknown]: 'Unknown', + [RepLevel.RepLevelHated]: 'Hated', + [RepLevel.RepLevelHostile]: 'Hostile', + [RepLevel.RepLevelUnfriendly]: 'Unfriendly', + [RepLevel.RepLevelNeutral]: 'Neutral', + [RepLevel.RepLevelFriendly]: 'Friendly', + [RepLevel.RepLevelHonored]: 'Honored', + [RepLevel.RepLevelRevered]: 'Revered', + [RepLevel.RepLevelExalted]: 'Exalted', +}; + +export const REP_FACTION_NAMES: Record = { + [RepFaction.RepFactionUnknown]: 'Unknown', +};