Implement teambuilder dropdown selector

This took way too long to get to a presentable state.

- search.js has been refactored into battle-search.ts (search logic)
  and battle-searchresults.tsx (display)

- panel-teambuilder.tsx has been split into teambuilder (team list) and
  teambuilder-team (team editor).

- The teambuilder's text editor can now detect which line it's on,
  and show the appropriate search result panel.

- The teambuilder's text editor now detects sets dynamically, and has
  the beginnings of support for set comments.

Currently, everything here is really basic, and mostly just a tech
demo for people to play around with and understand the direction of
the new teambuilder, but it'll be improved over time.
This commit is contained in:
Guangcong Luo 2019-06-24 23:57:25 +09:00
parent 26fa6f71e1
commit b47708efa0
12 changed files with 2222 additions and 129 deletions

View File

@ -13,6 +13,8 @@ node_modules/
/js/battle-animations.js
/js/battle-tooltips.js
/js/battle-scene-stub.js
/js/battle-search.js
/js/battle-searchresults.js
/js/client-core.js
/js/client-main.js
/js/client-connection.js
@ -23,4 +25,5 @@ node_modules/
/js/panel-rooms.js
/js/panel-chat.js
/js/panel-teambuilder.js
/js/panel-teambuilder-team.js
/js/panel-teamdropdown.js

3
.gitignore vendored
View File

@ -24,6 +24,8 @@ package-lock.json
/js/battle-animations.js
/js/battle-tooltips.js
/js/battle-scene-stub.js
/js/battle-search.js
/js/battle-searchresults.js
/js/client-core.js
/js/client-main.js
/js/client-connection.js
@ -34,6 +36,7 @@ package-lock.json
/js/panel-rooms.js
/js/panel-chat.js
/js/panel-teambuilder.js
/js/panel-teambuilder-team.js
/js/panel-teamdropdown.js
/replays/caches/

View File

@ -48,7 +48,7 @@
linkStyle("/style/sim-types.css");
linkStyle("/style/battle.css");
linkStyle("/style/teambuilder.css");
linkStyle("/style/utilichart.css");
linkStyle("style/battle-search.css");
linkStyle("/style/font-awesome.css");
</script>
<script defer src="/js/client-core.js?"></script>
@ -73,7 +73,12 @@
<script defer src="/data/moves.js?"></script>
<script defer src="/data/items.js?"></script>
<script defer src="/data/abilities.js?"></script>
<script defer src="/data/search-index.js?"></script>
<script defer src="/data/teambuilder-tables.js?"></script>
<script defer src="/js/panel-teamdropdown.js"></script>
<script defer src="/js/panel-teambuilder.js?"></script>
<script defer src="/js/battle-search.js?"></script>
<script defer src="/js/battle-searchresults.js?"></script>
<script defer src="/js/panel-teambuilder-team.js?"></script>
</body></html>

View File

@ -622,25 +622,9 @@ const Dex = new class implements ModdedDex {
return spriteData;
}
getPokemonIcon(pokemon: any, facingLeft?: boolean) {
getPokemonIconNum(id: ID, isFemale?: boolean, facingLeft?: boolean) {
let num = 0;
if (pokemon === 'pokeball') {
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/smicons-pokeball-sheet.png) no-repeat scroll -0px 4px';
} else if (pokemon === 'pokeball-statused') {
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/smicons-pokeball-sheet.png) no-repeat scroll -40px 4px';
} else if (pokemon === 'pokeball-fainted') {
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/smicons-pokeball-sheet.png) no-repeat scroll -80px 4px;opacity:.4;filter:contrast(0)';
} else if (pokemon === 'pokeball-none') {
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/smicons-pokeball-sheet.png) no-repeat scroll -80px 4px';
}
let id = toID(pokemon);
if (pokemon && pokemon.species) id = toID(pokemon.species);
if (pokemon && pokemon.volatiles && pokemon.volatiles.formechange && !pokemon.volatiles.transform) {
id = toID(pokemon.volatiles.formechange[1]);
}
if (pokemon && pokemon.num) {
num = pokemon.num;
} else if (window.BattlePokemonSprites && BattlePokemonSprites[id] && BattlePokemonSprites[id].num) {
if (window.BattlePokemonSprites && BattlePokemonSprites[id] && BattlePokemonSprites[id].num) {
num = BattlePokemonSprites[id].num;
} else if (window.BattlePokedex && window.BattlePokedex[id] && BattlePokedex[id].num) {
num = BattlePokedex[id].num;
@ -652,17 +636,36 @@ const Dex = new class implements ModdedDex {
num = BattlePokemonIconIndexes[id];
}
if (pokemon && pokemon.gender === 'F') {
if (id === 'unfezant' || id === 'frillish' || id === 'jellicent' || id === 'meowstic' || id === 'pyroar') {
if (isFemale) {
if (['unfezant', 'frillish', 'jellicent', 'meowstic', 'pyroar'].includes(id)) {
num = BattlePokemonIconIndexes[id + 'f'];
}
}
if (facingLeft) {
if (BattlePokemonIconIndexesLeft[id]) {
num = BattlePokemonIconIndexesLeft[id];
}
}
return num;
}
getPokemonIcon(pokemon: any, facingLeft?: boolean) {
if (pokemon === 'pokeball') {
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/smicons-pokeball-sheet.png) no-repeat scroll -0px 4px';
} else if (pokemon === 'pokeball-statused') {
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/smicons-pokeball-sheet.png) no-repeat scroll -40px 4px';
} else if (pokemon === 'pokeball-fainted') {
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/smicons-pokeball-sheet.png) no-repeat scroll -80px 4px;opacity:.4;filter:contrast(0)';
} else if (pokemon === 'pokeball-none') {
return 'background:transparent url(' + Dex.resourcePrefix + 'sprites/smicons-pokeball-sheet.png) no-repeat scroll -80px 4px';
}
let id = toID(pokemon);
if (pokemon && pokemon.species) id = toID(pokemon.species);
if (pokemon && pokemon.volatiles && pokemon.volatiles.formechange && !pokemon.volatiles.transform) {
id = toID(pokemon.volatiles.formechange[1]);
}
let num = this.getPokemonIconNum(id, pokemon && pokemon.gender === 'F', facingLeft);
let top = Math.floor(num / 12) * 30;
let left = (num % 12) * 40;

1227
src/battle-search.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,371 @@
/**
* Search Results
*
* Code for displaying sesrch results from battle-search.ts
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license AGPLv3
*/
class PSSearchResults extends preact.Component<{search: BattleSearch}> {
renderPokemonSortRow() {
const search = this.props.search;
const sortCol = search.sortCol;
return <li class="result"><div class="sortrow">
<button class={`sortcol numsortcol${!sortCol ? ' cur' : ''}`}>{!sortCol ? 'Sort: ' : (search.defaultResults && !search.filters ? 'Tier' : 'Number')}</button>
<button class={`sortcol pnamesortcol${sortCol === 'name' ? ' cur' : ''}`} data-sort="name">Name</button>
<button class={`sortcol typesortcol${sortCol === 'type' ? ' cur' : ''}`} data-sort="type">Types</button>
<button class={`sortcol abilitysortcol${sortCol === 'ability' ? ' cur' : ''}`} data-sort="ability">Abilities</button>
<button class={`sortcol statsortcol${sortCol === 'hp' ? ' cur' : ''}`} data-sort="hp">HP</button>
<button class={`sortcol statsortcol${sortCol === 'atk' ? ' cur' : ''}`} data-sort="atk">Atk</button>
<button class={`sortcol statsortcol${sortCol === 'def' ? ' cur' : ''}`} data-sort="def">Def</button>
<button class={`sortcol statsortcol${sortCol === 'spa' ? ' cur' : ''}`} data-sort="spa">SpA</button>
<button class={`sortcol statsortcol${sortCol === 'spd' ? ' cur' : ''}`} data-sort="spd">SpD</button>
<button class={`sortcol statsortcol${sortCol === 'spe' ? ' cur' : ''}`} data-sort="spe">Spe</button>
<button class={`sortcol statsortcol${sortCol === 'bst' ? ' cur' : ''}`} data-sort="bst">BST</button>
</div></li>;
}
renderMoveSortRow() {
const sortCol = this.props.search.sortCol;
return <li class="result"><div class="sortrow">
<button class={`sortcol movenamesortcol${sortCol === 'name' ? ' cur' : ''}`} data-sort="name">Name</button>
<button class={`sortcol movetypesortcol${sortCol === 'type' ? ' cur' : ''}`} data-sort="type">Type</button>
<button class={`sortcol movetypesortcol${sortCol === 'category' ? ' cur' : ''}`} data-sort="category">Cat</button>
<button class={`sortcol powersortcol${sortCol === 'power' ? ' cur' : ''}`} data-sort="power">Pow</button>
<button class={`sortcol accuracysortcol${sortCol === 'accuracy' ? ' cur' : ''}`} data-sort="accuracy">Acc</button>
<button class={`sortcol ppsortcol${sortCol === 'pp' ? ' cur' : ''}`} data-sort="pp">PP</button>
</div></li>;
}
renderPokemonRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const pokemon = search.dex.getTemplate(id);
if (!pokemon) return <li class="result">Unrecognized pokemon</li>;
let tagStart = (pokemon.forme ? pokemon.name.length - pokemon.forme.length - 1 : 0);
const stats = pokemon.baseStats;
let bst = 0;
for (const stat of Object.values(stats)) bst += stat;
if (search.gen < 2) bst -= stats['spd'];
if (errorMessage) {
return <li class="result"><a href={`${search.urlRoot}pokemon/${id}`} data-target="push" data-entry={`pokemon|${pokemon.name}`}>
<span class="col numcol">{search.getTier(pokemon)}</span>
<span class="col iconcol">
<span style={Dex.getPokemonIcon(pokemon)}></span>
</span>
<span class="col pokemonnamecol">{this.renderName(pokemon.name, matchStart, matchEnd, tagStart)}</span>
{errorMessage}
</a></li>;
}
return <li class="result"><a href={`${search.urlRoot}pokemon/${id}`} data-target="push" data-entry={`pokemon|${pokemon.name}`}>
<span class="col numcol">{search.getTier(pokemon)}</span>
<span class="col iconcol">
<span style={Dex.getPokemonIcon(pokemon)}></span>
</span>
<span class="col pokemonnamecol">{this.renderName(pokemon.name, matchStart, matchEnd, tagStart)}</span>
<span class="col typecol">
{pokemon.types.map(type =>
<img src={`${Dex.resourcePrefix}sprites/types/${type}.png`} alt={type} height="14" width="32" class="pixelated" />
)}
</span>
{search.gen >= 3 && (pokemon.abilities['1'] ?
<span class="col twoabilitycol">{pokemon.abilities['0']}<br />{pokemon.abilities['1']}</span>
:
<span class="col abilitycol">{pokemon.abilities['0']}</span>
)}
{search.gen >= 5 && (pokemon.abilities['S'] ?
<span class={`col twoabilitycol${pokemon.unreleasedHidden ? ' unreleasedhacol' : ''}`}>{pokemon.abilities['H'] || ''}<br />{pokemon.abilities['S']}</span>
: pokemon.abilities['H'] ?
<span class={`col abilitycol${pokemon.unreleasedHidden ? ' unreleasedhacol' : ''}`}>{pokemon.abilities['H']}</span>
:
<span class="col abilitycol"></span>
)}
<span class="col statcol"><em>HP</em><br />{stats.hp}</span>
<span class="col statcol"><em>Atk</em><br />{stats.atk}</span>
<span class="col statcol"><em>Def</em><br />{stats.def}</span>
{search.gen > 2 && <span class="col statcol"><em>SpA</em><br />{stats.spa}</span>}
{search.gen > 2 && <span class="col statcol"><em>SpD</em><br />{stats.spd}</span>}
{search.gen < 2 && <span class="col statcol"><em>Spc</em><br />{stats.spa}</span>}
<span class="col statcol"><em>Spe</em><br />{stats.spe}</span>
<span class="col bstcol"><em>BST<br />{bst}</em></span>
</a></li>;
}
renderName(name: string, matchStart: number, matchEnd: number, tagStart?: number) {
if (!matchEnd) {
if (!tagStart) return name;
return [
name.slice(0, tagStart), <small>{name.slice(tagStart)}</small>,
];
}
let output: preact.ComponentChild[];
if (tagStart && matchStart >= tagStart) {
output = [name];
} else {
output = [
name.slice(0, matchStart),
<b>{name.slice(matchStart, matchEnd)}</b>,
name.slice(matchEnd, tagStart || name.length),
];
if (!tagStart) return output;
}
if (matchEnd && matchEnd > tagStart) {
if (matchStart < tagStart) {
matchStart = tagStart;
}
output.push(
<small>{name.slice(tagStart, matchStart)}<b>{name.slice(matchStart, matchEnd)}</b>{name.slice(matchEnd)}</small>
);
} else {
output.push(<small>{name.slice(tagStart)}</small>);
}
return output;
}
renderItemRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const item = search.dex.getItem(id);
if (!item) return <li class="result">Unrecognized item</li>;
return <li class="result"><a href={`${search.urlRoot}items/${id}`} data-target="push" data-entry={`item|${item.name}`}>
<span class="col itemiconcol">
<span style={Dex.getItemIcon(item)}></span>
</span>
<span class="col namecol">{this.renderName(item.name, matchStart, matchEnd)}</span>
{errorMessage}
{!errorMessage && <span class="col itemdesccol">{item.shortDesc}</span>}
</a></li>;
}
renderAbilityRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const ability = search.dex.getAbility(id);
if (!ability) return <li class="result">Unrecognized ability</li>;
return <li class="result"><a href={`${search.urlRoot}abilitys/${id}`} data-target="push" data-entry={`ability|${ability.name}`}>
<span class="col namecol">{this.renderName(ability.name, matchStart, matchEnd)}</span>
{errorMessage}
{!errorMessage && <span class="col abilitydesccol">{ability.shortDesc}</span>}
</a></li>;
}
renderMoveRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const move = search.dex.getMove(id);
if (!move) return <li class="result">Unrecognized move</li>;
const tagStart = (move.name.startsWith('Hidden Power') ? 12 : 0);
if (errorMessage) {
return <li class="result"><a href={`${search.urlRoot}move/${id}`} data-target="push" data-entry={`move|${move.name}`}>
<span class="col movenamecol">{this.renderName(move.name, matchStart, matchEnd, tagStart)}</span>
{errorMessage}
</a></li>;
}
return <li class="result"><a href={`${search.urlRoot}move/${id}`} data-target="push" data-entry={`move|${move.name}`}>
<span class="col movenamecol">{this.renderName(move.name, matchStart, matchEnd, tagStart)}</span>
<span class="col typecol">
<img src={`${Dex.resourcePrefix}sprites/types/${move.type}.png`} alt={move.type} height="14" width="32" class="pixelated" />
<img src={`${Dex.resourcePrefix}sprites/categories/${move.category}.png`} alt={move.category} height="14" width="32" class="pixelated" />
</span>
<span class="col labelcol">
{move.category !== 'Status' ? [<em>Power</em>, <br />, `${move.basePower}` || '\u2014'] : ''}
</span>
<span class="col widelabelcol">
<em>Accuracy</em><br />${move.accuracy && move.accuracy !== true ? `${move.accuracy}%` : '\u2014'}
</span>
<span class="col pplabelcol">
<em>PP</em><br />{move.pp === 1 || move.noPPBoosts ? move.pp : move.pp * 8 / 5}
</span>
<span class="col movedesccol">{move.shortDesc}</span>
</a></li>;
}
renderTypeRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const name = id.charAt(0).toUpperCase() + id.slice(1);
return <li class="result"><a href={`${search.urlRoot}types/${id}`} data-target="push" data-entry={`type|${name}`}>
<span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span>
<span class="col typecol">
<img src={`${Dex.resourcePrefix}sprites/types/${name}.png`} alt={name} height="14" width="32" class="pixelated" />
</span>
{errorMessage}
</a></li>;
}
renderCategoryRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const name = id.charAt(0).toUpperCase() + id.slice(1);
return <li class="result"><a href={`${search.urlRoot}categories/${id}`} data-target="push" data-entry={`category|${name}`}>
<span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span>
<span class="col typecol">
<img src={`${Dex.resourcePrefix}sprites/categories/${name}.png`} alt={name} height="14" width="32" class="pixelated" />
</span>
{errorMessage}
</a></li>;
}
renderArticleRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
const isSearchType = (id === 'pokemon' || id === 'moves');
const name = (window.BattleArticleTitles && window.BattleArticleTitles[id]) ||
(id.charAt(0).toUpperCase() + id.substr(1));
return <li class="result"><a href={`${search.urlRoot}articles/${id}`} data-target="push" data-entry={`article|${name}`}>
<span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span>
<span class="col movedesccol">{isSearchType ? "(search type)" : "(article)"}</span>
{errorMessage}
</a></li>;
}
renderEggGroupRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
// very hardcode
let name: string | undefined;
if (id === 'humanlike') name = 'Human-Like';
else if (id === 'water1') name = 'Water 1';
else if (id === 'water2') name = 'Water 2';
else if (id === 'water3') name = 'Water 3';
if (name) {
if (matchEnd > 5) matchEnd++;
} else {
name = id.charAt(0).toUpperCase() + id.slice(1);
}
return <li class="result"><a href={`${search.urlRoot}egggroups/${id}`} data-target="push" data-entry={`egggroup|${name}`}>
<span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span>
<span class="col movedesccol">(egg group)</span>
{errorMessage}
</a></li>;
}
renderTierRow(id: ID, matchStart: number, matchEnd: number, errorMessage?: preact.ComponentChildren) {
const search = this.props.search;
// very hardcode
const tierTable: {[id: string]: string} = {
uber: "Uber",
lcuber: "LC Uber",
caplc: "CAP LC",
capnfe: "CAP NFE",
};
const name = tierTable[id] || id.toUpperCase();
return <li class="result"><a href={`${search.urlRoot}tiers/${id}`} data-target="push" data-entry={`tier|${name}`}>
<span class="col namecol">{this.renderName(name, matchStart, matchEnd)}</span>
<span class="col movedesccol">(tier)</span>
{errorMessage}
</a></li>;
}
renderRow(row: SearchRow) {
const search = this.props.search;
const [type, id] = row;
let matchStart = 0;
let matchEnd = 0;
if (row.length > 3) {
matchStart = row[2]!;
matchEnd = row[3]!;
}
let errorMessage: preact.ComponentChild = null;
if (search.qType && search.qType !== type) {
errorMessage = <span class="col filtercol"><em>Filter</em></span>;
} else if (search.legalityFilter && !(id in search.legalityFilter)) {
errorMessage = <span class="col illegalcol"><em>{search.legalityLabel}</em></span>;
}
switch (type) {
case 'html':
const sanitizedHTML = id.replace(/</g, '&lt;')
.replace(/&lt;em>/g, '<em>').replace(/&lt;\/em>/g, '</em>')
.replace(/&lt;strong>/g, '<strong>').replace(/&lt;\/strong>/g, '</strong>');
return <li class="result">
<p dangerouslySetInnerHTML={{__html: sanitizedHTML}}></p>
</li>;
case 'header':
return <li class="result"><h3>{id}</h3></li>;
case 'sortpokemon':
return this.renderPokemonSortRow();
case 'sortmove':
return this.renderMoveSortRow();
case 'pokemon':
return this.renderPokemonRow(id as ID, matchStart, matchEnd, errorMessage);
case 'move':
return this.renderMoveRow(id as ID, matchStart, matchEnd, errorMessage);
case 'item':
return this.renderItemRow(id as ID, matchStart, matchEnd, errorMessage);
case 'ability':
return this.renderAbilityRow(id as ID, matchStart, matchEnd, errorMessage);
case 'type':
return this.renderTypeRow(id as ID, matchStart, matchEnd, errorMessage);
case 'egggroup':
return this.renderEggGroupRow(id as ID, matchStart, matchEnd, errorMessage);
case 'tier':
return this.renderTierRow(id as ID, matchStart, matchEnd, errorMessage);
case 'category':
return this.renderCategoryRow(id as ID, matchStart, matchEnd, errorMessage);
case 'article':
return this.renderArticleRow(id as ID, matchStart, matchEnd, errorMessage);
}
return <li>Error: not found</li>;
}
render() {
const search = this.props.search;
return <div class="searchresults"><ul class="dexlist">
{search.filters && <p>
Filters: {}
{search.filters.map(([type, name]) =>
<button class="filter" value={`${type}:${name}`}>
${name} <i class="fa fa-times-circle"></i>
</button>
)}
{!search.q && <small style="color: #888">(backspace = delete filter)</small>}
</p>}
{search.results &&
// TODO: implement windowing
// for now, just show first ten results
search.results.slice(0, 10).map(result =>
this.renderRow(result)
)}
</ul></div>;
}
}

2
src/globals.d.ts vendored
View File

@ -42,8 +42,6 @@ declare var exports: any;
type AnyObject = {[k: string]: any};
declare var app: {user: AnyObject, rooms: AnyObject, ignore?: AnyObject};
declare var BattleSearch: any;
interface Window {
[k: string]: any;
}

View File

@ -0,0 +1,197 @@
/**
* Teambuilder team panel
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license AGPLv3
*/
class TeamTextbox extends preact.Component<{sets: PokemonSet[]}> {
setInfo: {
species: string,
bottomY: number,
}[] = [];
textbox: HTMLTextAreaElement = null!;
heightTester: HTMLTextAreaElement = null!;
activeType: 'pokemon' | 'move' | 'item' | 'ability' | '' = '';
activeOffsetY = -1;
search = new BattleSearch();
getYAt(index: number, value: string) {
if (index < 0) return 10;
this.heightTester.value = value.slice(0, index);
return this.heightTester.scrollHeight;
}
input = () => this.update();
select = () => this.update(true);
update = (cursorOnly?: boolean) => {
const textbox = this.textbox;
this.heightTester.style.width = `${textbox.offsetWidth}px`;
const value = textbox.value;
let index = 0;
let setIndex = -1;
if (!cursorOnly) this.setInfo = [];
this.activeOffsetY = -1;
this.activeType = '';
const selectionStart = textbox.selectionStart || 0;
const selectionEnd = textbox.selectionEnd || 0;
/** 0 = set top, 1 = set middle */
let parseState: 0 | 1 = 0;
while (index < value.length) {
let nlIndex = value.indexOf('\n', index);
if (nlIndex < 0) nlIndex = value.length;
const line = value.slice(index, nlIndex).trim();
if (!line) {
parseState = 0;
index = nlIndex + 1;
continue;
}
if (parseState === 0 && index && !cursorOnly) {
this.setInfo[this.setInfo.length - 1].bottomY = this.getYAt(index - 1, value);
}
if (parseState === 0) {
if (!cursorOnly) {
const atIndex = line.indexOf('@');
let species = atIndex >= 0 ? line.slice(0, atIndex).trim() : line;
if (species.endsWith(')')) {
const parenIndex = species.lastIndexOf(' (');
if (parenIndex >= 0) {
species = species.slice(parenIndex + 2, -1);
}
}
this.setInfo.push({
species,
bottomY: -1,
});
}
parseState = 1;
setIndex++;
}
const selectionEndCutoff = (selectionStart === selectionEnd ? nlIndex : nlIndex + 1);
if (index <= selectionStart && selectionEnd <= selectionEndCutoff) {
// both ends within range
this.activeOffsetY = this.getYAt(index - 1, value);
const lcLine = line.toLowerCase().trim();
if (lcLine.startsWith('ability:')) {
this.activeType = 'ability';
} else if (lcLine.startsWith('-')) {
this.activeType = 'move';
} else if (
!lcLine || lcLine.startsWith('ivs:') || lcLine.startsWith('evs:') ||
lcLine.startsWith('level:') || lcLine.startsWith('gender:') ||
lcLine.endsWith(' nature') || lcLine.startsWith('shiny:')
) {
// leave activeType blank
} else {
this.activeType = 'pokemon';
}
this.search.setType(this.activeType, 'gen7ou' as ID, this.props.sets[setIndex]);
this.search.find('');
}
index = nlIndex + 1;
}
if (!cursorOnly) {
const bottomY = this.getYAt(value.length, value);
if (this.setInfo.length) {
this.setInfo[this.setInfo.length - 1].bottomY = bottomY;
}
textbox.style.height = `${bottomY + 100}px`;
}
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.input} onSelect={this.select} onClick={this.select} onKeyUp={this.select} />
<textarea
class="textbox teamtextbox heighttester" style="visibility:hidden" tabIndex={-1} aria-hidden={true}
/>
<div class="teamoverlays">
{this.setInfo.slice(0, -1).map(info =>
<hr style={`top:${info.bottomY - 18}px`} />
)}
{this.setInfo.map((info, i) => {
if (!info.species) return null;
const prevOffset = i === 0 ? 8 : this.setInfo[i - 1].bottomY;
const species = info.species;
const num = Dex.getPokemonIconNum(toID(species));
if (!num) return null;
const top = Math.floor(num / 12) * 30;
const left = (num % 12) * 40;
const iconStyle = `background:transparent url(${Dex.resourcePrefix}sprites/smicons-sheet.png?a5) no-repeat scroll -${left}px -${top}px`;
return <span class="picon" style={
`top:${prevOffset + 1}px;left:50px;position:absolute;${iconStyle}`
}></span>;
})}
{this.activeOffsetY >= 0 &&
<div class="teaminnertextbox" style={{top: this.activeOffsetY - 1}}></div>
}
</div>
{this.activeType && <PSSearchResults search={this.search} />}
</div>;
}
}
class TeamPanel extends PSRoomPanel {
sets: PokemonSet[] | null = null;
backToList = () => {
PS.removeRoom(this.props.room);
PS.join('teambuilder' as RoomID);
};
render() {
const room = this.props.room;
const team = PS.teams.byKey[room.id.slice(5)];
if (!team) {
return <PSPanelWrapper room={room}>
<button class="button" onClick={this.backToList}>
<i class="fa fa-chevron-left"></i> List
</button>
<p class="error">
Team doesn't exist
</p>
</PSPanelWrapper>;
}
const sets = this.sets || PSTeambuilder.unpackTeam(team!.packedTeam);
if (!this.sets) this.sets = sets;
return <PSPanelWrapper room={room} scrollable>
<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>;
}
}
PS.roomTypes['team'] = {
Model: PSRoom,
Component: TeamPanel,
title: "Team",
};
PS.updateRoomTypes();

View File

@ -1,5 +1,5 @@
/**
* Teambuilder Panel
* Teambuilder panel
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license AGPLv3
@ -38,7 +38,7 @@ class TeambuilderPanel extends PSRoomPanel {
/**
* Folders, in a format where lexical sort will sort correctly.
*/
let folders = [];
let folders: string[] = [];
for (let i = -2; i < PS.teams.list.length; i++) {
const team = i >= 0 ? PS.teams.list[i] : null;
if (team) {
@ -99,7 +99,7 @@ class TeambuilderPanel extends PSRoomPanel {
</TeamFolder>,
];
let renderedFolders = [];
let renderedFolders: preact.ComponentChild[] = [];
for (let format of folders) {
let newGen = '';
@ -220,108 +220,8 @@ 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 = () => {
PS.removeRoom(this.props.room);
PS.join('teambuilder' as RoomID);
};
render() {
const room = this.props.room;
const team = PS.teams.byKey[room.id.slice(5)];
if (!team) {
return <PSPanelWrapper room={room}>
<button class="button" onClick={this.backToList}>
<i class="fa fa-chevron-left"></i> List
</button>
<p class="error">
Team doesn't exist
</p>
</PSPanelWrapper>;
}
const sets = this.sets || PSTeambuilder.unpackTeam(team!.packedTeam);
if (!this.sets) this.sets = sets;
return <PSPanelWrapper room={room} scrollable>
<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>;
}
}
PS.roomTypes['teambuilder'] = {
Model: PSRoom,
Component: TeambuilderPanel,
title: "Teambuilder",
};
PS.roomTypes['team'] = {
Model: PSRoom,
Component: TeamPanel,
title: "Team",
};
PS.updateRoomTypes();

354
style/battle-search.css Normal file
View File

@ -0,0 +1,354 @@
.dexlist, .dexlist li {
list-style-type: none;
margin: 0;
padding: 0;
display: block;
clear: both;
word-wrap: normal;
}
.dexlist li > a {
display: block;
height: 30px;
padding: 0 0 0 4px;
border: 1px solid transparent;
margin: 0 5px 1px 5px;
text-decoration: none;
}
.dexlist li > a.cur {
border-color: #CCCCCC;
background: #F2F2F2;
}
.dexlist .filter,
.searchboxwrapper .filter {
padding:1px 3px;
border-radius: 3px;
border: 1px solid #777;
background: #EEE;
color: black;
font-size: 9pt;
font-family: Verdana, sans-serif;
font-weight: bold;
cursor: pointer;
}
.dexlist .filter:hover,
.searchboxwrapper .filter:hover {
color: #777777;
text-decoration: line-through;
}
.searchboxwrapper .filter.noclear:hover {
color: black;
text-decoration: none;
cursor: default;
}
.dexlist .filter i,
.searchboxwrapper .filter i {
color: #999999;
}
.dexlist .filter:hover i,
.searchboxwrapper .filter:hover i {
color: #BB2222;
}
.resultheader,
.dexlist li.resultheader,
.dexlist .result {
height: 32px;
padding: 1px 0 0 0;
}
.result p,
.resultheader p,
.dexlist .result p {
padding: 7px 0 0 8px;
margin: 0;
}
.dexlist h3,
.dexentry h3,
.resultheader h3 {
margin: 5px 0 0 -1px;
padding: 3px 8px;
font-family: Verdana, sans-serif;
font-size: 10pt;
background: #DAE5F0;
color: black;
border: 1px solid #AAAAAA;
text-shadow: 1px 1px 0 rgba(255,255,255,.6);
/* box-shadow: inset 1px 1px 0 rgba(255,255,255,.6), 0px 2px 2px rgba(0,0,0,.15); */
box-shadow: inset 1px 1px 0 rgba(255,255,255,.6);
}
.dexlist li > em {
display: block;
padding: 0 8px;
font-size: 10pt;
}
.dexlist .col {
float: left;
padding-top: 7px;
height: 22px;
font-size: 9pt;
color: #444444;
}
.dexlist .iconcol {
padding-top: 0px;
width: 40px;
height: 30px;
}
.dexlist .iconcol span {
display: block;
width: 40px;
height: 30px;
background: transparent none no-repeat scroll 0px -8px;
}
.dexlist .itemiconcol {
padding-top: 3px;
width: 24px;
height: 27px;
}
.dexlist .itemiconcol span {
display: block;
width: 24px;
height: 24px;
background: transparent none no-repeat scroll 0px 0px;
}
.dexlist .numcol {
width: 24px;
padding-right: 5px;
text-align: right;
color: #999999;
padding-top: 8px;
font-size: 8pt;
white-space: nowrap;
}
.dexlist .statcol,
.dexlist .bstcol,
.dexlist .labelcol,
.dexlist .widelabelcol,
.dexlist .pplabelcol {
padding-top: 1px;
height: 28px;
width: 24px;
text-align: right;
font-size: 8pt;
}
.dexlist .labelcol {
text-align: center;
width: 30px;
margin-left: -2px;
}
.dexlist .widelabelcol {
text-align: center;
width: 50px;
}
.dexlist .pplabelcol {
text-align: center;
width: 18px;
padding-right: 5px;
}
.dexlist .bstcol {
padding-left: 2px;
}
.dexlist .labelcol em,
.dexlist .widelabelcol em,
.dexlist .pplabelcol em,
.dexlist .statcol em,
.dexlist .bstcol em {
color: #999999;
font-size: 7pt;
font-style: normal;
}
.dexlist .stattitlecol {
padding-top: 2px;
width: 20px;
text-align: right;
font-size: 7pt;
color: #999999;
}
.dexlist .namecol {
padding-top: 6px;
height: 23px;
width: 120px;
color: #000000;
font-size: 10pt;
}
.dexlist .pokemonnamecol {
padding-top: 6px;
height: 23px;
width: 127px;
color: #000000;
font-size: 10pt;
white-space: nowrap;
}
.dexlist .shortpokemonnamecol {
padding-top: 6px;
height: 23px;
width: 118px;
color: #000000;
font-size: 10pt;
white-space: nowrap;
}
.dexlist .movenamecol {
padding-top: 6px;
height: 23px;
width: 152px;
color: #000000;
font-size: 10pt;
}
.dexlist .shortmovenamecol {
padding-top: 6px;
height: 23px;
width: 112px;
color: #000000;
font-size: 10pt;
}
.dexlist .tagcol {
padding-top: 6px;
height: 24px;
width: 34px;
border-right: 1px solid #CCCCCC;
margin-right: 5px;
text-align: center;
color: #000000;
font-size: 10pt;
}
.dexlist .tagcol.shorttagcol {
margin-right: 1px;
}
.dexlist .typecol {
width: 70px;
}
.dexlist .abilitycol {
padding-top: 8px;
height: 21px;
font-size: 8pt;
width: 86px;
text-align: center;
white-space: nowrap;
}
.dexlist .twoabilitycol {
padding-top: 1px;
height: 28px;
font-size: 8pt;
width: 86px;
text-align: center;
white-space: nowrap;
}
.dexlist .hacol {
font-style: italic;
}
.dexlist .unreleasedhacol {
font-style: italic;
text-decoration: line-through;
}
.dexlist b {
color: #4488CC;
text-decoration: underline;
}
.dexlist small {
font-size: 8pt;
}
.dexlist small b {
font-weight: normal;
}
.dexlist .illegalcol em {
padding: 2px 4px;
height: auto;
border: 1px solid #AA2222;
color: #AA2222;
border-radius: 4px;
}
.dexlist .filtercol em {
padding: 2px 4px;
height: auto;
border: 1px solid #22AA22;
color: #22AA22;
border-radius: 4px;
}
.dexlist .typecol img {
opacity: 0.6;
margin-right: 1px;
}
.dexlist .typecol img.b {
opacity: 1;
outline: #4488CC solid 1px;
outline-offset: 0;
background: #4488CC;
}
.dexlist .itemdesccol,
.dexlist .abilitydesccol,
.dexlist .movedesccol {
white-space: nowrap;
padding-top: 8px;
font-size: 8pt;
width: 488px;
height: 21px;
overflow: hidden;
color: #777777;
}
.dexlist .movedesccol {
width: 284px;
}
.dexlist .itemdesccol {
width: 464px;
}
.dexlist .sortrow {
background: #D0D0D0;
border-bottom: 1px solid #888;
height: 20px;
color: black;
}
.dexlist .sortcol {
font-family: Verdana, sans-serif;
font-size: 8pt;
background: transparent;
color: black;
float: left;
border-radius: 0;
border: 0;
padding: 0 0 0 0;
text-align: center;
height: 20px;
}
.dexlist .sortcol.cur {
font-weight: bold;
background: #E0E0E0;
}
.dexlist .sortcol:hover {
background: #F0F0F0;
}
.dexlist .sortcol.numsortcol.cur,
.dexlist .sortcol.numsortcol.cur:hover {
background: #D0D0D0;
cursor: default;
}
.dexlist .numsortcol {
width: 80px;
}
.dexlist .pnamesortcol {
width: 127px;
}
.dexlist .typesortcol {
width: 70px;
}
.dexlist .abilitysortcol {
width: 172px;
}
.dexlist .statsortcol {
width: 24px;
text-align: right;
}
.dexlist .movenamesortcol {
width: 159px;
}
.dexlist .movetypesortcol {
width: 35px;
}
.dexlist .powersortcol {
width: 30px;
}
.dexlist .accuracysortcol {
width: 49px;
}
.dexlist .ppsortcol {
width: 23px;
}

View File

@ -3,6 +3,33 @@
padding-top: 8px;
padding-left: 100px;
box-sizing: border-box;
line-height: 20px;
}
.teaminnertextbox {
width: 320px;
height: 18px;
left: 96px;
position: absolute;
pointer-events: none;
border: 1px solid #4488CC;
border-radius: 3px;
box-shadow: inset 0px 1px 2px #D2D2D2, 0px 0px 5px #66AADD;
background: transparent;
}
.searchresults {
background: #f2f2f2;
border: 1px solid #888888;
border-radius: 2px;
position: fixed;
bottom: 5px;
left: 5px;
width: 640px;
min-height: 200px;
max-height: 400px;
overflow: auto;
}
.teameditor {

View File

@ -48,7 +48,7 @@
linkStyle("style/sim-types.css");
linkStyle("style/battle.css");
linkStyle("style/teambuilder.css");
linkStyle("style/utilichart.css");
linkStyle("style/battle-search.css");
linkStyle("style/font-awesome.css");
</script>
<script src="config/testclient-key.js"></script>
@ -74,7 +74,12 @@
<script src="data/moves.js"></script>
<script src="data/items.js"></script>
<script src="data/abilities.js"></script>
<script src="data/search-index.js"></script>
<script src="data/teambuilder-tables.js"></script>
<script src="js/panel-teamdropdown.js"></script>
<script src="js/panel-teambuilder.js"></script>
<script src="js/panel-teambuilder.js?"></script>
<script src="js/battle-search.js?"></script>
<script src="js/battle-searchresults.js?"></script>
<script src="js/panel-teambuilder-team.js?"></script>
</body></html>