Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add points remaining and copy to talents #4219

Merged
merged 1 commit into from
Feb 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading