mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-07-07 04:06:51 -05:00
Improve teambuilder stub
This should give an idea of the direction I want to take the teambuilder in: a text editor, but with special features. This neatly sidesteps a lot of UI questions, especially all the export and input buttons, because the regular editing mode can be used to import and export without anything fancy. This also makes way for a better way to implement team/set comments: comments are just invalid Pokémon in a team.
This commit is contained in:
parent
9d3122cf92
commit
bb6fe7ec3e
|
|
@ -47,6 +47,7 @@
|
|||
}
|
||||
linkStyle("/style/sim-types.css");
|
||||
linkStyle("/style/battle.css");
|
||||
linkStyle("/style/teambuilder.css");
|
||||
linkStyle("/style/utilichart.css");
|
||||
linkStyle("/style/font-awesome.css");
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -22,21 +22,22 @@ class BattleLog {
|
|||
className: string;
|
||||
battleParser: BattleTextParser | null = null;
|
||||
/**
|
||||
* -1 = spectator: "Red sent out Pikachu!" "Blue's Eevee used Tackle!"
|
||||
* 0 = player 1: "Go! Pikachu!" "The opposing Eevee used Tackle!"
|
||||
* 1 = player 2: "Red sent out Pikachu!" "Eevee used Tackle!"
|
||||
* * -1 = spectator: "Red sent out Pikachu!" "Blue's Eevee used Tackle!"
|
||||
* * 0 = player 1: "Go! Pikachu!" "The opposing Eevee used Tackle!"
|
||||
* * 1 = player 2: "Red sent out Pikachu!" "Eevee used Tackle!"
|
||||
*/
|
||||
perspective: -1 | 0 | 1 = -1;
|
||||
constructor(elem: HTMLDivElement, scene?: BattleScene) {
|
||||
constructor(elem: HTMLDivElement, scene?: BattleScene | null, innerElem?: HTMLDivElement) {
|
||||
this.elem = elem;
|
||||
|
||||
elem.setAttribute('role', 'log');
|
||||
elem.innerHTML = '';
|
||||
const innerElem = document.createElement('div');
|
||||
innerElem.className = 'inner';
|
||||
elem.appendChild(innerElem);
|
||||
if (!innerElem) {
|
||||
elem.setAttribute('role', 'log');
|
||||
elem.innerHTML = '';
|
||||
innerElem = document.createElement('div');
|
||||
innerElem.className = 'inner';
|
||||
elem.appendChild(innerElem);
|
||||
}
|
||||
this.innerElem = innerElem;
|
||||
this.className = elem.className;
|
||||
|
||||
if (scene) {
|
||||
this.scene = scene;
|
||||
|
|
@ -47,6 +48,7 @@ class BattleLog {
|
|||
this.battleParser = new BattleTextParser();
|
||||
}
|
||||
|
||||
this.className = elem.className;
|
||||
elem.onscroll = this.onScroll;
|
||||
}
|
||||
onScroll = () => {
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ class ChatTextEntry extends preact.Component<{room: PSRoom, onMessage: (msg: str
|
|||
};
|
||||
focusIfNoSelection = (e: Event) => {
|
||||
if ((e.target as HTMLElement).tagName === 'TEXTAREA') return;
|
||||
const selection = window.getSelection();
|
||||
const selection = window.getSelection()!;
|
||||
if (selection.type === 'Range') return;
|
||||
const elem = this.base!.children[0].children[1] as HTMLTextAreaElement;
|
||||
elem.focus();
|
||||
|
|
|
|||
|
|
@ -137,8 +137,11 @@ class TeambuilderPanel extends PSRoomPanel {
|
|||
} else {
|
||||
formatName = BattleLog.escapeHTML(formatName);
|
||||
}
|
||||
renderedFolders.push(<TeamFolder cur={this.curFolder === format} value={format}>
|
||||
<i class={'fa ' + (this.curFolder === format ? 'fa-folder-open' : 'fa-folder') + (format === '/' ? '-o' : '')}></i>
|
||||
const isCurFolder = this.curFolder === format;
|
||||
renderedFolders.push(<TeamFolder cur={isCurFolder} value={format}>
|
||||
<i class={
|
||||
`fa ${isCurFolder ? 'fa-folder-open' : 'fa-folder'}${format === '/' ? '-o' : ''}`
|
||||
}></i>
|
||||
{formatName}
|
||||
</TeamFolder>);
|
||||
continue;
|
||||
|
|
@ -217,6 +220,64 @@ class TeambuilderPanel extends PSRoomPanel {
|
|||
}
|
||||
}
|
||||
|
||||
class TeamTextbox extends preact.Component<{sets: PokemonSet[]}> {
|
||||
separators: number[] = [];
|
||||
textbox: HTMLTextAreaElement = null!;
|
||||
heightTester: HTMLTextAreaElement = null!;
|
||||
update = () => {
|
||||
const textbox = this.textbox;
|
||||
const heightTester = this.heightTester;
|
||||
heightTester.style.width = `${textbox.offsetWidth}px`;
|
||||
const value = textbox.value;
|
||||
|
||||
let separatorIndex = value.indexOf('\n\n');
|
||||
const separators: number[] = [];
|
||||
while (separatorIndex >= 0) {
|
||||
while (value.charAt(separatorIndex + 2) === '\n') separatorIndex++;
|
||||
heightTester.value = value.slice(0, separatorIndex);
|
||||
separators.push(heightTester.scrollHeight);
|
||||
|
||||
separatorIndex = value.indexOf('\n\n', separatorIndex + 1);
|
||||
}
|
||||
|
||||
heightTester.value = textbox.value;
|
||||
textbox.style.height = `${heightTester.scrollHeight + 100}px`;
|
||||
this.separators = separators;
|
||||
this.forceUpdate();
|
||||
};
|
||||
componentDidMount() {
|
||||
this.textbox = this.base!.getElementsByClassName('teamtextbox')[0] as HTMLTextAreaElement;
|
||||
this.heightTester = this.base!.getElementsByClassName('heighttester')[0] as HTMLTextAreaElement;
|
||||
|
||||
const exportedTeam = PSTeambuilder.exportTeam(this.props.sets);
|
||||
this.textbox.value = exportedTeam;
|
||||
this.update();
|
||||
}
|
||||
componentWillUnmount() {
|
||||
this.textbox = null!;
|
||||
this.heightTester = null!;
|
||||
}
|
||||
render() {
|
||||
return <div class="teameditor">
|
||||
<textarea class="textbox teamtextbox" onInput={this.update} />
|
||||
<textarea
|
||||
class="textbox teamtextbox heighttester" style="visibility:hidden" tabIndex={-1} aria-hidden={true}
|
||||
/>
|
||||
<div class="teamoverlays">
|
||||
{this.separators.map(offset =>
|
||||
<hr style={`top:${offset}px`} />
|
||||
)}
|
||||
{this.props.sets.map((set, i) => {
|
||||
const prevOffset = i === 0 ? -5 : this.separators[i - 1];
|
||||
return <span class="picon" style={
|
||||
`top:${prevOffset + 10}px;left:50px;position:absolute;` + Dex.getPokemonIcon(set.species)
|
||||
}></span>;
|
||||
})}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
class TeamPanel extends PSRoomPanel {
|
||||
sets: PokemonSet[] | null = null;
|
||||
backToList = () => {
|
||||
|
|
@ -240,13 +301,15 @@ class TeamPanel extends PSRoomPanel {
|
|||
const sets = this.sets || PSTeambuilder.unpackTeam(team!.packedTeam);
|
||||
if (!this.sets) this.sets = sets;
|
||||
return <PSPanelWrapper room={room} scrollable>
|
||||
<button class="button" onClick={this.backToList}>
|
||||
<i class="fa fa-chevron-left"></i> List
|
||||
</button>
|
||||
<h2>
|
||||
{team.name}
|
||||
</h2>
|
||||
<pre>{PSTeambuilder.exportTeam(sets)}</pre>
|
||||
<div class="pad">
|
||||
<button class="button" onClick={this.backToList}>
|
||||
<i class="fa fa-chevron-left"></i> List
|
||||
</button>
|
||||
<h2>
|
||||
{team.name}
|
||||
</h2>
|
||||
<TeamTextbox sets={sets} />
|
||||
</div>
|
||||
</PSPanelWrapper>;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -240,84 +240,97 @@ class PSTeambuilder {
|
|||
|
||||
return team;
|
||||
}
|
||||
/**
|
||||
* (Exports end with two spaces so linebreaks are preserved in Markdown;
|
||||
* I assume mostly for Reddit.)
|
||||
*/
|
||||
static exportSet(set: PokemonSet) {
|
||||
let text = '';
|
||||
|
||||
// core
|
||||
if (set.name && set.name !== set.species) {
|
||||
text += `${set.name} (${set.species})`;
|
||||
} else {
|
||||
text += `${set.species}`;
|
||||
}
|
||||
if (set.gender === 'M') text += ` (M)`;
|
||||
if (set.gender === 'F') text += ` (F)`;
|
||||
if (set.item) {
|
||||
text += ` @ ${set.item}`;
|
||||
}
|
||||
text += ` \n`;
|
||||
if (set.ability) {
|
||||
text += `Ability: ${set.ability} \n`;
|
||||
}
|
||||
if (set.moves) {
|
||||
for (let move of set.moves) {
|
||||
if (move.substr(0, 13) === 'Hidden Power ') {
|
||||
const hpType = move.slice(13);
|
||||
move = move.slice(0, 13);
|
||||
move = `${move}[${hpType}]`;
|
||||
}
|
||||
if (move) {
|
||||
text += `- ${move} \n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stats
|
||||
let first = true;
|
||||
if (set.evs) {
|
||||
for (const stat of Dex.statNames) {
|
||||
if (!set.evs[stat]) continue;
|
||||
if (first) {
|
||||
text += `EVs: `;
|
||||
first = false;
|
||||
} else {
|
||||
text += ` / `;
|
||||
}
|
||||
text += `${set.evs[stat]} ${BattleStatNames[stat]}`;
|
||||
}
|
||||
}
|
||||
if (!first) {
|
||||
text += ` \n`;
|
||||
}
|
||||
if (set.nature) {
|
||||
text += `${set.nature} Nature \n`;
|
||||
}
|
||||
first = true;
|
||||
if (set.ivs) {
|
||||
for (const stat of Dex.statNames) {
|
||||
if (set.ivs[stat] === undefined || isNaN(set.ivs[stat]) || set.ivs[stat] === 31) continue;
|
||||
if (first) {
|
||||
text += `IVs: `;
|
||||
first = false;
|
||||
} else {
|
||||
text += ` / `;
|
||||
}
|
||||
text += `${set.ivs[stat]} ${BattleStatNames[stat]}`;
|
||||
}
|
||||
}
|
||||
if (!first) {
|
||||
text += ` \n`;
|
||||
}
|
||||
|
||||
// details
|
||||
if (set.level && set.level !== 100) {
|
||||
text += `Level: ${set.level} \n`;
|
||||
}
|
||||
if (set.shiny) {
|
||||
text += `Shiny: Yes \n`;
|
||||
}
|
||||
if (typeof set.happiness === 'number' && set.happiness !== 255 && !isNaN(set.happiness)) {
|
||||
text += `Happiness: ${set.happiness} \n`;
|
||||
}
|
||||
|
||||
text += `\n`;
|
||||
return text;
|
||||
}
|
||||
static exportTeam(sets: PokemonSet[]) {
|
||||
let text = '';
|
||||
for (const set of sets) {
|
||||
// core
|
||||
if (set.name && set.name !== set.species) {
|
||||
text += '' + set.name + ' (' + set.species + ')';
|
||||
} else {
|
||||
text += '' + set.species;
|
||||
}
|
||||
if (set.gender === 'M') text += ' (M)';
|
||||
if (set.gender === 'F') text += ' (F)';
|
||||
if (set.item) {
|
||||
text += ' @ ' + set.item;
|
||||
}
|
||||
text += " \n";
|
||||
if (set.ability) {
|
||||
text += 'Ability: ' + set.ability + " \n";
|
||||
}
|
||||
if (set.moves) {
|
||||
for (let move of set.moves) {
|
||||
if (move.substr(0, 13) === 'Hidden Power ') {
|
||||
move = move.substr(0, 13) + '[' + move.substr(13) + ']';
|
||||
}
|
||||
if (move) {
|
||||
text += '- ' + move + " \n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// stats
|
||||
let first = true;
|
||||
if (set.evs) {
|
||||
for (const stat of Dex.statNames) {
|
||||
if (!set.evs[stat]) continue;
|
||||
if (first) {
|
||||
text += 'EVs: ';
|
||||
first = false;
|
||||
} else {
|
||||
text += ' / ';
|
||||
}
|
||||
text += '' + set.evs[stat] + ' ' + BattleStatNames[stat];
|
||||
}
|
||||
}
|
||||
if (!first) {
|
||||
text += " \n";
|
||||
}
|
||||
if (set.nature) {
|
||||
text += '' + set.nature + ' Nature' + " \n";
|
||||
}
|
||||
first = true;
|
||||
if (set.ivs) {
|
||||
for (const stat of Dex.statNames) {
|
||||
if (set.ivs[stat] === undefined || isNaN(set.ivs[stat]) || set.ivs[stat] === 31) continue;
|
||||
if (first) {
|
||||
text += 'IVs: ';
|
||||
first = false;
|
||||
} else {
|
||||
text += ' / ';
|
||||
}
|
||||
text += '' + set.ivs[stat] + ' ' + BattleStatNames[stat];
|
||||
}
|
||||
}
|
||||
if (!first) {
|
||||
text += " \n";
|
||||
}
|
||||
|
||||
// details
|
||||
if (set.level && set.level !== 100) {
|
||||
text += 'Level: ' + set.level + " \n";
|
||||
}
|
||||
if (set.shiny) {
|
||||
text += 'Shiny: Yes \n';
|
||||
}
|
||||
if (typeof set.happiness === 'number' && set.happiness !== 255 && !isNaN(set.happiness)) {
|
||||
text += 'Happiness: ' + set.happiness + " \n";
|
||||
}
|
||||
|
||||
text += "\n";
|
||||
text += PSTeambuilder.exportSet(set);
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
|
|
|||
25
style/teambuilder.css
Normal file
25
style/teambuilder.css
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
.teamtextbox {
|
||||
width: 100%;
|
||||
padding-top: 8px;
|
||||
padding-left: 100px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.teameditor {
|
||||
position: relative;
|
||||
}
|
||||
.teameditor .heighttester {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
overflow-y: hidden;
|
||||
height: 10px;
|
||||
/* I have no clue what the default <textarea> box-sizing is but "content-box" doesn't mean what you'd think */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.teameditor hr {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
left: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
|
@ -47,6 +47,7 @@
|
|||
}
|
||||
linkStyle("style/sim-types.css");
|
||||
linkStyle("style/battle.css");
|
||||
linkStyle("style/teambuilder.css");
|
||||
linkStyle("style/utilichart.css");
|
||||
linkStyle("style/font-awesome.css");
|
||||
</script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user