Skip to content

Commit

Permalink
add points remaining and copy to talents (#4219)
Browse files Browse the repository at this point in the history
  • Loading branch information
kayla-glick authored Feb 18, 2024
1 parent 85f3887 commit 802cd64
Show file tree
Hide file tree
Showing 10 changed files with 195 additions and 74 deletions.
55 changes: 55 additions & 0 deletions ui/core/components/copy_button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Tooltip } from 'bootstrap';
// eslint-disable-next-line unused-imports/no-unused-imports
import { element, ref } from 'tsx-vanilla';

import { Component } from "./component";

export interface CopyButtonConfig {
getContent: () => string,
extraCssClasses?: string[],
text?: string,
tooltip?: string,
}

export class CopyButton extends Component {
private readonly config: CopyButtonConfig;

constructor(parent: HTMLElement, config: CopyButtonConfig) {
const btnRef = ref<HTMLButtonElement>();
const buttonElem = (
<button
className={`btn ${config.extraCssClasses?.join(' ') ?? ''}`}
ref={btnRef}
>
<i className="fas fa-copy me-1" />{config.text ?? 'Copy to Clipboard'}
</button>
)

super(parent, 'copy-button', buttonElem as HTMLElement);
this.config = config;

const button = btnRef.value!
let clicked = false
button.addEventListener('click', _event => {
if (clicked) return

const data = this.config.getContent()
if (navigator.clipboard == undefined) {
alert(data);
} else {
clicked = true
navigator.clipboard.writeText(data);
const originalContent = button.innerHTML;
button.innerHTML = '<i class="fas fa-check me-1"></i>Copied';
setTimeout(() => {
button.innerHTML = originalContent;
clicked = false
}, 1500);
}
});

if (config.tooltip) {
Tooltip.getOrCreateInstance(button, {title: config.tooltip});
}
}
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// eslint-disable-next-line unused-imports/no-unused-imports
import { element, ref } from 'tsx-vanilla';

import { IndividualSimUI } from '../individual_sim_ui';
import { SimUI } from '../sim_ui';
import {
Expand All @@ -15,7 +18,9 @@ import { IndividualLinkImporter, IndividualWowheadGearPlannerImporter } from './
import { RaidSimRequest } from '../proto/api';
import { SimSettingCategories } from '../sim';
import { EventID, TypedEvent } from '../typed_event';

import { BooleanPicker } from './boolean_picker';
import { CopyButton } from './copy_button';

import * as Mechanics from '../constants/mechanics';

Expand All @@ -37,41 +42,26 @@ export abstract class Exporter extends BaseModal {
this.body.innerHTML = `
<textarea spellCheck="false" class="exporter-textarea form-control"></textarea>
`;
this.footer!.innerHTML = `
<button class="exporter-button btn btn-primary clipboard-button me-2">
<i class="fas fa-clipboard"></i>
Copy to Clipboard
</button>
${options.allowDownload ? `
<button class="exporter-button btn btn-primary download-button">
<i class="fa fa-download"></i>
Download
</button>
` : ''
}
`;

this.textElem = this.rootElem.getElementsByClassName('exporter-textarea')[0] as HTMLElement;

const clipboardButton = this.rootElem.getElementsByClassName('clipboard-button')[0] as HTMLElement;
clipboardButton.addEventListener('click', event => {
const data = this.textElem.textContent!;
if (navigator.clipboard == undefined) {
alert(data);
} else {
navigator.clipboard.writeText(data);
const originalContent = clipboardButton.innerHTML;
clipboardButton.style.width = `${clipboardButton.getBoundingClientRect().width.toFixed(3)}px`;
clipboardButton.innerHTML = `<i class="fas fa-check"></i>&nbsp;Copied`;
setTimeout(() => {
clipboardButton.innerHTML = originalContent;
}, 1500);
}
});
new CopyButton(this.footer!, {
extraCssClasses: ['btn-primary', 'me-2'],
getContent: () => this.textElem.innerHTML,
text: 'Copy',
tooltip: 'Copy to clipboard',
})

if (options.allowDownload) {
const downloadButton = this.rootElem.getElementsByClassName('download-button')[0] as HTMLElement;
downloadButton.addEventListener('click', event => {
const downloadBtnRef = ref<HTMLButtonElement>()
this.footer!.appendChild(
<button className="exporter-button btn btn-primary download-button" ref={downloadBtnRef}>
<i className="fa fa-download me-1"></i>
Download
</button>
);

const downloadButton = downloadBtnRef.value!;
downloadButton.addEventListener('click', _event => {
const data = this.textElem.textContent!;
downloadString(data, 'wowsims.json');
});
Expand Down
2 changes: 1 addition & 1 deletion ui/core/components/individual_sim_ui/talents_tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export class TalentsTab extends SimTab {

private buildHunterPetPicker(parentElem: HTMLElement) {
new HunterPetTalentsPicker(parentElem, this.simUI, this.simUI.player as Player<Spec.SpecHunter>);
const petTypeToggle = new IconEnumPicker(parentElem, this.simUI.player as Player<Spec.SpecHunter>, makePetTypeInputConfig(false));
new IconEnumPicker(parentElem, this.simUI.player as Player<Spec.SpecHunter>, makePetTypeInputConfig());
}

private buildSavedTalentsPicker() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// eslint-disable-next-line unused-imports/no-unused-imports
import { element } from 'tsx-vanilla';

import { Glyphs } from '../proto/common.js';
import { ItemQuality } from '../proto/common.js';
import { ActionId } from '../proto_utils/action_id.js';
Expand Down Expand Up @@ -48,6 +51,10 @@ 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 majorGlyphs = Object.keys(glyphsConfig.majorGlyphs).map(idStr => Number(idStr));
const minorGlyphs = Object.keys(glyphsConfig.minorGlyphs).map(idStr => Number(idStr));

Expand Down Expand Up @@ -116,7 +123,7 @@ class GlyphPicker extends Input<Player<any>, number> {
this.iconElem = this.rootElem.getElementsByClassName('glyph-picker-icon')[0] as HTMLAnchorElement;
this.iconElem.addEventListener('click', event => {
event.preventDefault();
const selectorModal = new GlyphSelectorModal(this.rootElem.closest('.individual-sim-ui')!, this, this.glyphOptions);
new GlyphSelectorModal(this.rootElem.closest('.individual-sim-ui')!, this, this.glyphOptions);
});

this.init();
Expand Down Expand Up @@ -199,7 +206,6 @@ class GlyphSelectorModal extends BaseModal {

const applyFilters = () => {
let validItemElems = listItemElems;
const selectedGlyph = glyphPicker.selectedGlyph;

validItemElems = validItemElems.filter(elem => {
const listItemIdx = parseInt(elem.dataset.idx!);
Expand Down
10 changes: 4 additions & 6 deletions ui/core/talents/hunter_pet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { Spec } from '../proto/common.js';
import { HunterPetTalents, Hunter_Options_PetType as PetType } from '../proto/hunter.js';
import { Player } from '../player.js';
import { Component } from '../components/component.js';
import { EnumPicker, EnumPickerConfig } from '../components/enum_picker.js';
import { SavedDataManager } from '../components/saved_data_manager.js';
import { EventID, TypedEvent } from '../typed_event.js';
import { ActionId } from '../proto_utils/action_id.js';
Expand All @@ -17,11 +16,10 @@ import HunterPetCunningJson from './trees/hunter_cunning.json'
import HunterPetFerocityJson from './trees/hunter_ferocity.json'
import HunterPetTenacityJson from './trees/hunter_tenacity.json'

export function makePetTypeInputConfig(includeLabel: boolean): InputHelpers.TypedIconEnumPickerConfig<Player<Spec.SpecHunter>, PetType> {
export function makePetTypeInputConfig(): InputHelpers.TypedIconEnumPickerConfig<Player<Spec.SpecHunter>, PetType> {
return InputHelpers.makeSpecOptionsEnumIconInput<Spec.SpecHunter, PetType>({
fieldName: 'petType',
numColumns: 5,
//label: includeLabel ? 'Pet' : '',
values: [
{ value: PetType.PetNone, tooltip: 'No Pet' },
{ actionId: ActionId.fromPetName('Bat'), tooltip: 'Bat', value: PetType.Bat },
Expand Down Expand Up @@ -142,7 +140,7 @@ export class HunterPetTalentsPicker extends Component {
klass: player.getClass(),
trees: talentsConfig,
changedEvent: (player: Player<Spec.SpecHunter>) => player.specOptionsChangeEmitter,
getValue: (player: Player<Spec.SpecHunter>) => protoToTalentString(this.getPetTalentsFromPlayer(), talentsConfig),
getValue: (_player: Player<Spec.SpecHunter>) => protoToTalentString(this.getPetTalentsFromPlayer(), talentsConfig),
setValue: (eventID: EventID, player: Player<Spec.SpecHunter>, newValue: string) => {
const options = player.getSpecOptions();
options.petTalents = talentStringToProto(HunterPetTalents.create(), newValue, talentsConfig);
Expand All @@ -159,7 +157,7 @@ export class HunterPetTalentsPicker extends Component {
presetsOnly: true,
label: 'Pet Talents',
storageKey: '__NEVER_USED__',
getData: (player: Player<Spec.SpecHunter>) => protoToTalentString(this.getPetTalentsFromPlayer(), talentsConfig),
getData: (_player: Player<Spec.SpecHunter>) => protoToTalentString(this.getPetTalentsFromPlayer(), talentsConfig),
setData: (eventID: EventID, player: Player<Spec.SpecHunter>, newValue: string) => {
const options = player.getSpecOptions();
options.petTalents = talentStringToProto(HunterPetTalents.create(), newValue, talentsConfig);
Expand All @@ -171,7 +169,7 @@ export class HunterPetTalentsPicker extends Component {
changeEmitters: [this.player.specOptionsChangeEmitter],
equals: (a: string, b: string) => a == b,
toJson: (a: string) => a,
fromJson: (obj: any) => '',
fromJson: (_obj: any) => '',
});
savedTalentsManager.addSavedData({
name: 'Default',
Expand Down
83 changes: 57 additions & 26 deletions ui/core/talents/talents_picker.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { Carousel, Tooltip } from 'bootstrap';
// eslint-disable-next-line unused-imports/no-unused-imports
import { element, fragment, ref } from 'tsx-vanilla';
import { Tooltip } from 'bootstrap';

import { Component } from '../components/component.js';
import { CopyButton } from '../components/copy_button.js';
import { Input, InputConfig } from '../components/input.js';
import { Class, Spec } from '../proto/common.js';
import { ActionId } from '../proto_utils/action_id.js';
Expand All @@ -9,21 +13,19 @@ import { isRightClick } from '../utils.js';
import { sum } from '../utils.js';
import { Player } from '../player.js';

import { element, fragment } from 'tsx-vanilla';

const MAX_POINTS_PLAYER = 71;
const MAX_POINTS_HUNTER_PET = 16;
const MAX_POINTS_HUNTER_PET_BM = 20;

export interface TalentsPickerConfig<ModObject, TalentsProto> extends InputConfig<ModObject, string> {
export interface TalentsPickerConfig<TalentsProto> extends InputConfig<Player<Spec>, string> {
klass: Class,
trees: TalentsConfig<TalentsProto>,
pointsPerRow: number,
maxPoints: number,
pointsPerRow: number,
trees: TalentsConfig<TalentsProto>,
}

export class TalentsPicker<ModObject, TalentsProto> extends Input<ModObject, string> {
private readonly config: TalentsPickerConfig<ModObject, TalentsProto>;
export class TalentsPicker<TalentsProto> extends Input<Player<Spec>, string> {
private readonly config: TalentsPickerConfig<TalentsProto>;

readonly numRows: number;
readonly numCols: number;
Expand All @@ -32,29 +34,58 @@ export class TalentsPicker<ModObject, TalentsProto> extends Input<ModObject, str

readonly trees: Array<TalentTreePicker<TalentsProto>>;

constructor(parent: HTMLElement, modObject: ModObject, config: TalentsPickerConfig<ModObject, TalentsProto>) {
super(parent, 'talents-picker-root', modObject, { ...config, inline: true });
constructor(parent: HTMLElement, player: Player<Spec>, config: TalentsPickerConfig<TalentsProto>) {
super(parent, 'talents-picker-root', player, { ...config, inline: true });
this.config = config;
this.pointsPerRow = config.pointsPerRow;
this.maxPoints = config.maxPoints;
this.numRows = Math.max(...config.trees.map(treeConfig => treeConfig.talents.map(talentConfig => talentConfig.location.rowIdx).flat()).flat()) + 1;
this.numCols = Math.max(...config.trees.map(treeConfig => treeConfig.talents.map(talentConfig => talentConfig.location.colIdx).flat()).flat()) + 1;
this.maxPoints = config.maxPoints

const pointsRemainingElemRef = ref<HTMLSpanElement>();
const getPointsRemaining = () => this.maxPoints - player.getTalentTreePoints().reduce((sum, points) => sum + points, 0);

const PointsRemainingElem = () => {
const pointsRemaining = getPointsRemaining();
return <span className="talent-tree-points" ref={pointsRemainingElemRef}>{pointsRemaining}</span>
}

TypedEvent.onAny([player.talentsChangeEmitter]).on(() => {
pointsRemainingElemRef.value!.replaceWith(PointsRemainingElem())
});

const actionsContainerRef = ref<HTMLDivElement>();
this.rootElem.appendChild(
<div id="talents-carousel" className="carousel slide">
<div className="carousel-inner">
<>
<div className="talents-picker-header">
<div>
<label>Points Remaining:</label>
{PointsRemainingElem()}
</div>
<div className="talents-picker-actions" ref={actionsContainerRef}></div>
</div>
<button className="carousel-control-prev" type="button">
<span className="carousel-control-prev-icon" attributes={{'aria-hidden':true}}></span>
<span className="visually-hidden">Previous</span>
</button>
<button className="carousel-control-next" type="button">
<span className="carousel-control-next-icon" attributes={{'aria-hidden':true}}></span>
<span className="visually-hidden">Next</span>
</button>
</div>
<div id="talents-carousel" className="carousel slide">
<div className="carousel-inner">
</div>
<button className="carousel-control-prev" type="button">
<span className="carousel-control-prev-icon" attributes={{'aria-hidden':true}}></span>
<span className="visually-hidden">Previous</span>
</button>
<button className="carousel-control-next" type="button">
<span className="carousel-control-next-icon" attributes={{'aria-hidden':true}}></span>
<span className="visually-hidden">Next</span>
</button>
</div>
</>
);

new CopyButton(actionsContainerRef.value!, {
extraCssClasses: ['btn-sm', 'btn-outline-primary', 'copy-talents'],
getContent: () => player.getTalentsString(),
text: "Copy",
tooltip: "Copy talent string",
});

const carouselContainer = this.rootElem.querySelector('.carousel-inner') as HTMLElement;
const carouselPrevBtn = this.rootElem.querySelector('.carousel-control-prev') as HTMLButtonElement;
const carouselNextBtn = this.rootElem.querySelector('.carousel-control-next') as HTMLButtonElement;
Expand Down Expand Up @@ -147,12 +178,12 @@ class TalentTreePicker<TalentsProto> extends Component {
private readonly pointsElem: HTMLElement;

readonly talents: Array<TalentPicker<TalentsProto>>;
readonly picker: TalentsPicker<any, TalentsProto>;
readonly picker: TalentsPicker<TalentsProto>;

// The current number of points in this tree
numPoints: number;

constructor(parent: HTMLElement, config: TalentTreeConfig<TalentsProto>, picker: TalentsPicker<any, TalentsProto>, klass: Class, specNumber: number) {
constructor(parent: HTMLElement, config: TalentTreeConfig<TalentsProto>, picker: TalentsPicker<TalentsProto>, klass: Class, specNumber: number) {
super(parent, 'talent-tree-picker-root');
this.config = config;
this.numPoints = 0;
Expand Down Expand Up @@ -219,7 +250,7 @@ class TalentTreePicker<TalentsProto> extends Component {
new Tooltip(resetBtn, {
title: 'Reset talent points',
});
resetBtn.addEventListener('click', event => {
resetBtn.addEventListener('click', _event => {
this.talents.forEach(talent => talent.setPoints(0, false));
this.picker.inputChanged(TypedEvent.nextEventID());
});
Expand Down Expand Up @@ -350,7 +381,7 @@ class TalentPicker<TalentsProto> extends Component {
this.rootElem.addEventListener('contextmenu', event => {
event.preventDefault();
});
this.rootElem.addEventListener('touchmove', event => {
this.rootElem.addEventListener('touchmove', _event => {
if (this.longTouchTimer != undefined) {
clearTimeout(this.longTouchTimer);
this.longTouchTimer = undefined;
Expand Down
2 changes: 1 addition & 1 deletion ui/hunter/inputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const WeaponAmmo = InputHelpers.makeSpecOptionsEnumIconInput<Spec.SpecHun
],
});

export const PetTypeInput = makePetTypeInputConfig(true);
export const PetTypeInput = makePetTypeInputConfig();

export const PetUptime = InputHelpers.makeSpecOptionsNumberInput<Spec.SpecHunter>({
fieldName: 'petUptime',
Expand Down
6 changes: 5 additions & 1 deletion ui/scss/core/components/_exporters.scss
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.exporter {
.exporter-textarea {
width: 100%;
height: 40vh;
height: 60vh;
resize: none;
}

Expand All @@ -10,4 +10,8 @@
grid-template-columns: repeat(3,1fr);
column-gap: 1.25rem;
}

button {
width: 12rem;
}
}
Loading

0 comments on commit 802cd64

Please sign in to comment.