Skip to content

Commit

Permalink
Merge pull request #16 from wowsims/cata-talents-glyphs
Browse files Browse the repository at this point in the history
Implement cataclysm talents/glyphs UX
  • Loading branch information
kayla-glick authored Mar 17, 2024
2 parents 4b6647d + 3baec8a commit ee88517
Show file tree
Hide file tree
Showing 30 changed files with 977 additions and 899 deletions.
2 changes: 1 addition & 1 deletion ui/core/components/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ export abstract class Component {
protected customRootElement?(): HTMLElement;

private disposeCallbacks: Array<() => void> = [];
private disposed: boolean = false;
private disposed = false;

readonly rootElem: HTMLElement;

Expand Down
27 changes: 7 additions & 20 deletions ui/core/components/individual_sim_ui/talents_tab.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
import * as Mechanics from '../../constants/mechanics';
import { IndividualSimUI } from '../../individual_sim_ui';
import { Player } from '../../player';
import { Class, Glyphs, Spec } from '../../proto/common';
import { SavedTalents } from '../../proto/ui';
import { HunterSpecs } from '../../proto_utils/utils';
import { classGlyphsConfig, classTalentsConfig } from '../../talents/factory';
import { GlyphsPicker } from '../../talents/glyphs_picker';
import { HunterPetTalentsPicker, makePetTypeInputConfig } from '../../talents/hunter_pet';
import { classTalentsConfig } from '../../talents/factory';
import { HunterPetTalentsPicker } from '../../talents/hunter_pet';
import { TalentsPicker } from '../../talents/talents_picker';
import { EventID, TypedEvent } from '../../typed_event';
import { IconEnumPicker } from '../icon_enum_picker';
import { SavedDataManager } from '../saved_data_manager';
import { SimTab } from '../sim_tab';

Expand Down Expand Up @@ -39,7 +35,6 @@ export class TalentsTab<SpecType extends Spec> extends SimTab {
this.buildHunterPickers();
} else {
this.buildTalentsPicker(this.leftPanel);
this.buildGlyphsPicker(this.leftPanel);
}

this.buildSavedTalentsPicker();
Expand All @@ -55,14 +50,9 @@ export class TalentsTab<SpecType extends Spec> extends SimTab {
player.setTalentsString(eventID, newValue);
},
pointsPerRow: 5,
maxPoints: Mechanics.MAX_TALENT_POINTS,
});
}

private buildGlyphsPicker(parentElem: HTMLElement) {
new GlyphsPicker(parentElem, this.simUI.player, classGlyphsConfig[this.simUI.player.getClass()]);
}

private buildHunterPickers() {
this.leftPanel.innerHTML = `
<div class="hunter-talents-pickers-container tab-content">
Expand Down Expand Up @@ -105,16 +95,13 @@ export class TalentsTab<SpecType extends Spec> extends SimTab {
const petTab = this.leftPanel.querySelector('#pet-talents-tab') as HTMLElement;

this.buildTalentsPicker(playerTab);
this.buildGlyphsPicker(playerTab);

if (this.simUI.player.getClass() == Class.ClassHunter) {
this.buildHunterPetPicker(petTab);
}
this.buildHunterPetPicker(petTab);
}

private buildHunterPetPicker<T extends HunterSpecs>(parentElem: HTMLElement) {
new HunterPetTalentsPicker(parentElem, this.simUI, this.simUI.player as unknown as Player<T>);
new IconEnumPicker(parentElem, this.simUI.player as unknown as Player<T>, makePetTypeInputConfig());
private buildHunterPetPicker(parentElem: HTMLElement) {
if (this.simUI.player.isClass(Class.ClassHunter)) {
new HunterPetTalentsPicker(parentElem, this.simUI, this.simUI.player);
}
}

private buildSavedTalentsPicker() {
Expand Down
2 changes: 1 addition & 1 deletion ui/core/components/sim_title_dropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export class SimTitleDropdown extends Component {
if (data.type == 'Raid') {
label = raidSimLabel;
} else if (data.type == 'Spec') {
label = data.spec.friendlyName;
label = PlayerSpecs.getFullSpecName(data.spec);
}

const fragment = document.createElement('fragment');
Expand Down
1 change: 0 additions & 1 deletion ui/core/constants/mechanics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export const CHARACTER_LEVEL = 85;
export const MAX_TALENT_POINTS = 41;
export const BOSS_LEVEL = CHARACTER_LEVEL + 3;

export const EXPERTISE_PER_QUARTER_PERCENT_REDUCTION = 32.79 / 4;
Expand Down
5 changes: 3 additions & 2 deletions ui/core/individual_sim_ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import { Stats } from './proto_utils/stats';
import { getTalentPoints, SpecOptions } from './proto_utils/utils';
import { SimSettingCategories } from './sim';
import { SimUI, SimWarning } from './sim_ui';
import { MAX_POINTS_PLAYER } from './talents/talents_picker';
import { EventID, TypedEvent } from './typed_event';

const SAVED_GEAR_STORAGE_KEY = '__savedGear__';
Expand Down Expand Up @@ -233,9 +234,9 @@ export abstract class IndividualSimUI<SpecType extends Spec> extends SimUI {
if (talentPoints == 0) {
// Just return here, so we don't show a warning during page load.
return '';
} else if (talentPoints < Mechanics.MAX_TALENT_POINTS) {
} else if (talentPoints < MAX_POINTS_PLAYER) {
return 'Unspent talent points.';
} else if (talentPoints > Mechanics.MAX_TALENT_POINTS) {
} else if (talentPoints > MAX_POINTS_PLAYER) {
return 'More than maximum talent points spent.';
} else {
return '';
Expand Down
6 changes: 1 addition & 5 deletions ui/core/player.ts
Original file line number Diff line number Diff line change
Expand Up @@ -869,11 +869,7 @@ export class Player<SpecType extends Spec> {
}

getPrimeGlyps(): Array<number> {
return [
this.glyphs.prime1,
this.glyphs.prime2,
this.glyphs.prime3,
].filter(glyph => glyph != 0)
return [this.glyphs.prime1, this.glyphs.prime2, this.glyphs.prime3].filter(glyph => glyph != 0);
}

getMajorGlyphs(): Array<number> {
Expand Down
3 changes: 3 additions & 0 deletions ui/core/player_specs/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,9 @@ export const PlayerSpecs = {
getFullSpecName: <SpecType extends Spec>(playerSpec: PlayerSpec<SpecType>): string => {
return `${playerSpec.friendlyName} ${getPlayerClass(playerSpec).friendlyName}`;
},
getSpecNumber: <SpecType extends Spec>(playerSpec: PlayerSpec<SpecType>): number => {
return Object.values(getPlayerClass(playerSpec).specs).findIndex(spec => spec == playerSpec) ?? 0;
},
// Prefixes used for storing browser data for each site. Even if a Spec is
// renamed, DO NOT change these values or people will lose their saved data.
getLocalStorageKey: <SpecType extends Spec>(playerSpec: PlayerSpec<SpecType>): string => {
Expand Down
133 changes: 85 additions & 48 deletions ui/core/talents/glyphs_picker.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,36 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import { element } from 'tsx-vanilla';
import { element, fragment, ref } from 'tsx-vanilla';

import { BaseModal } from '../components/base_modal.js';
import { Component } from '../components/component.js';
import { ContentBlock } from '../components/content_block.js';
import { Input } from '../components/input.js';
import { getLanguageCode } from '../constants/lang';
import { setItemQualityCssClass } from '../css_utils.js';
import { Player } from '../player.js';
import { Glyphs , ItemQuality } from '../proto/common.js';
import { Glyphs, ItemQuality } from '../proto/common.js';
import { ActionId } from '../proto_utils/action_id.js';
import { EventID, TypedEvent } from '../typed_event.js';
import { stringComparator } from '../utils.js';

export type GlyphConfig = {
name: string,
description: string,
iconUrl: string,
name: string;
description: string;
iconUrl: string;
};

export type GlyphsConfig = {
primeGlyphs: Record<number, GlyphConfig>,
majorGlyphs: Record<number, GlyphConfig>,
minorGlyphs: Record<number, GlyphConfig>,
primeGlyphs: Record<number, GlyphConfig>;
majorGlyphs: Record<number, GlyphConfig>;
minorGlyphs: Record<number, GlyphConfig>;
};

interface GlyphData {
id: number,
name: string,
description: string,
iconUrl: string,
quality: ItemQuality | null,
id: number;
name: string;
description: string;
iconUrl: string;
quality: ItemQuality | null;
}

const emptyGlyphData: GlyphData = {
Expand All @@ -51,10 +52,6 @@ export class GlyphsPicker extends Component {
super(parent, 'glyphs-picker-root');
this.glyphsConfig = glyphsConfig;

this.rootElem.appendChild(
<h6 className="mt-2 fw-bold d-xl-block d-none">Glyphs</h6>
)

const primeGlyphs = Object.keys(glyphsConfig.primeGlyphs).map(idStr => Number(idStr));
const majorGlyphs = Object.keys(glyphsConfig.majorGlyphs).map(idStr => Number(idStr));
const minorGlyphs = Object.keys(glyphsConfig.minorGlyphs).map(idStr => Number(idStr));
Expand All @@ -67,28 +64,28 @@ export class GlyphsPicker extends Component {
majorGlyphsData.sort((a, b) => stringComparator(a.name, b.name));
minorGlyphsData.sort((a, b) => stringComparator(a.name, b.name));

const primeGlyphsBlock = new ContentBlock(this.rootElem, 'major-glyphs', {
header: { title: 'Major', extraCssClasses: ['border-0', 'mb-1'] }
const primeGlyphsBlock = new ContentBlock(this.rootElem, 'prime-glyphs', {
header: { title: 'Prime Glyphs', extraCssClasses: ['border-0', 'mb-1'] },
});

const majorGlyphsBlock = new ContentBlock(this.rootElem, 'major-glyphs', {
header: { title: 'Major', extraCssClasses: ['border-0', 'mb-1'] }
header: { title: 'Major Glyphs', extraCssClasses: ['border-0', 'mb-1'] },
});

const minorGlyphsBlock = new ContentBlock(this.rootElem, 'minor-glyphs', {
header: { title: 'Minor', extraCssClasses: ['border-0', 'mb-1'] }
header: { title: 'Minor Glyphs', extraCssClasses: ['border-0', 'mb-1'] },
});

this.primeGlyphPickers = (['prime1', 'prime2', 'prime3'] as Array<keyof Glyphs>).map(glyphField => {
return new GlyphPicker(primeGlyphsBlock.bodyElement, player, primeGlyphsData, glyphField, true)
return new GlyphPicker(primeGlyphsBlock.bodyElement, player, primeGlyphsData, glyphField);
});

this.majorGlyphPickers = (['major1', 'major2', 'major3'] as Array<keyof Glyphs>).map(glyphField => {
return new GlyphPicker(majorGlyphsBlock.bodyElement, player, majorGlyphsData, glyphField, true)
return new GlyphPicker(majorGlyphsBlock.bodyElement, player, majorGlyphsData, glyphField);
});

this.minorGlyphPickers = (['minor1', 'minor2', 'minor3'] as Array<keyof Glyphs>).map(glyphField => {
return new GlyphPicker(minorGlyphsBlock.bodyElement, player, minorGlyphsData, glyphField, false)
return new GlyphPicker(minorGlyphsBlock.bodyElement, player, minorGlyphsData, glyphField);
});
}

Expand All @@ -109,13 +106,18 @@ export class GlyphsPicker extends Component {

class GlyphPicker extends Input<Player<any>, number> {
readonly player: Player<any>;
private readonly iconElem: HTMLAnchorElement;

selectedGlyph: GlyphData | undefined;

private readonly glyphOptions: Array<GlyphData>;
selectedGlyph: GlyphData;

constructor(parent: HTMLElement, player: Player<any>, glyphOptions: Array<GlyphData>, glyphField: keyof Glyphs, isMajor: boolean) {
private readonly anchorElem: HTMLAnchorElement;
private readonly iconElem: HTMLImageElement;
private readonly nameElem: HTMLSpanElement;

constructor(parent: HTMLElement, player: Player<any>, glyphOptions: Array<GlyphData>, glyphField: keyof Glyphs) {
super(parent, 'glyph-picker-root', player, {
inline: true,
changedEvent: (player: Player<any>) => player.glyphsChangeEmitter,
getValue: (player: Player<any>) => player.getGlyphs()[glyphField] as number,
setValue: (eventID: EventID, player: Player<any>, newValue: number) => {
Expand All @@ -124,20 +126,36 @@ class GlyphPicker extends Input<Player<any>, number> {
player.setGlyphs(eventID, glyphs);
},
});
if (!isMajor) {
this.rootElem.classList.add('minor');
}
this.rootElem.classList.add('item-picker-root');

this.player = player;
this.glyphOptions = glyphOptions;
this.selectedGlyph = emptyGlyphData;

this.rootElem.innerHTML = `<a class="glyph-picker-icon" data-whtticon='false'></a>`;
const anchorElemRef = ref<HTMLAnchorElement>();
const iconElemRef = ref<HTMLImageElement>();
const nameElemRef = ref<HTMLSpanElement>();

this.iconElem = this.rootElem.getElementsByClassName('glyph-picker-icon')[0] as HTMLAnchorElement;
this.iconElem.addEventListener('click', event => {
this.rootElem.appendChild(
<a ref={anchorElemRef} attributes={{ role: 'button' }} className="d-flex w-100">
<img ref={iconElemRef} className="item-picker-icon" />
<div className="item-picker-labels-container">
<span ref={nameElemRef} className="item-picker-name" />
</div>
</a>,
);

this.anchorElem = anchorElemRef.value!;
this.iconElem = iconElemRef.value!;
this.nameElem = nameElemRef.value!;

const openGlyphSelectorModal = (event: Event) => {
event.preventDefault();
new GlyphSelectorModal(this.rootElem.closest('.individual-sim-ui')!, this, this.glyphOptions);
});
};

this.iconElem.addEventListener('click', openGlyphSelectorModal);
this.nameElem.addEventListener('click', openGlyphSelectorModal);

this.init();
}
Expand All @@ -147,14 +165,34 @@ class GlyphPicker extends Input<Player<any>, number> {
}

getInputValue(): number {
return this.selectedGlyph.id;
return this.selectedGlyph?.id ?? 0;
}

setInputValue(newValue: number) {
this.selectedGlyph = this.glyphOptions.find(glyphData => glyphData.id == newValue) || emptyGlyphData;
this.selectedGlyph = this.glyphOptions.find(glyphData => glyphData.id == newValue);

if (this.selectedGlyph) {
const lang = getLanguageCode();
const langPrefix = lang ? `${lang}.` : '';

this.anchorElem.href = ActionId.makeItemUrl(this.selectedGlyph.id);
this.anchorElem.dataset.wowhead = `domain=${langPrefix}cata&dataEnv=11`;
this.anchorElem.dataset.whtticon = 'false';

this.iconElem.src = this.selectedGlyph.iconUrl;

this.nameElem.textContent = this.selectedGlyph.name;
} else {
this.clear();
}
}

private clear() {
this.anchorElem.removeAttribute('data-wowhead');
this.anchorElem.removeAttribute('href');

this.iconElem.style.backgroundImage = `url('${this.selectedGlyph.iconUrl}')`;
this.iconElem.href = this.selectedGlyph.id == 0 ? '' : ActionId.makeItemUrl(this.selectedGlyph.id);
this.iconElem.src = emptyGlyphData.iconUrl;
this.nameElem.textContent = emptyGlyphData.name;
}
}

Expand Down Expand Up @@ -187,7 +225,7 @@ class GlyphSelectorModal extends BaseModal {
</a>
`;

const anchorElem = listItemElem.children[0] as HTMLAnchorElement
const anchorElem = listItemElem.children[0] as HTMLAnchorElement;
const iconElem = listItemElem.querySelector('.selector-modal-list-item-icon') as HTMLImageElement;
const nameElem = listItemElem.querySelector('.selector-modal-list-item-name') as HTMLElement;

Expand All @@ -203,7 +241,7 @@ class GlyphSelectorModal extends BaseModal {
});

const updateSelected = () => {
const selectedGlyphId = glyphPicker.selectedGlyph.id;
const selectedGlyphId = glyphPicker.selectedGlyph?.id ?? 0;

listItemElems.forEach(elem => {
const listItemIdx = parseInt(elem.dataset.idx!);
Expand All @@ -225,13 +263,12 @@ class GlyphSelectorModal extends BaseModal {
const listItemData = glyphOptions[listItemIdx];

if (searchInput.value.length > 0) {
const searchQuery = searchInput.value.toLowerCase().split(" ");
const searchQuery = searchInput.value.toLowerCase().split(' ');
const name = listItemData.name.toLowerCase();

let include = true;
searchQuery.forEach(v => {
if (!name.includes(v))
include = false;
if (!name.includes(v)) include = false;
});
if (!include) {
return false;
Expand All @@ -241,16 +278,16 @@ class GlyphSelectorModal extends BaseModal {
return true;
});

listElem.innerHTML = ``
listElem.innerHTML = ``;
listElem.append(...validItemElems);
};

const searchInput = this.rootElem.getElementsByClassName('selector-modal-search')[0] as HTMLInputElement;
searchInput.addEventListener('input', applyFilters);
searchInput.addEventListener("keyup", ev => {
if (ev.key == "Enter") {
searchInput.addEventListener('keyup', ev => {
if (ev.key == 'Enter') {
listItemElems.find(ele => {
if (ele.classList.contains("hidden")) {
if (ele.classList.contains('hidden')) {
return false;
}
const nameElem = ele.getElementsByClassName('selector-modal-list-item-name')[0] as HTMLElement;
Expand Down
Loading

0 comments on commit ee88517

Please sign in to comment.