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:
Guangcong Luo 2019-05-16 05:30:27 +10:00
parent 9d3122cf92
commit bb6fe7ec3e
7 changed files with 199 additions and 94 deletions

View File

@ -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>

View File

@ -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 = () => {

View File

@ -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();

View File

@ -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>;
}
}

View File

@ -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
View 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;
}

View File

@ -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>