mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-03-21 17:50:29 -05:00
Add static website for viewing server-side teams
This commit is contained in:
parent
e8c60fd8c6
commit
8e378c6129
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -42,3 +42,8 @@ npm-debug.log
|
|||
/replay.pokemonshowdown.com/caches/
|
||||
/replay.pokemonshowdown.com/theme/wrapper.inc.php
|
||||
/replay.pokemonshowdown.com/ads.txt
|
||||
|
||||
/teams.pokemonshowdown.com/js/
|
||||
/teams.pokemonshowdown.com/caches/
|
||||
/teams.pokemonshowdown.com/index.html
|
||||
/teams.pokemonshowdown.com/ads.txt
|
||||
|
|
|
|||
|
|
@ -88,7 +88,7 @@ const compileOpts = Object.assign(eval('(' + fs.readFileSync('.babelrc') + ')'),
|
|||
if (process.argv[2] === 'full') {
|
||||
delete compileOpts.ignore;
|
||||
compiler.compileToDir(
|
||||
['caches/pokemon-showdown/server/chat-formatter.ts'],
|
||||
['caches/pokemon-showdown/server/chat-formatter.ts', 'caches/pokemon-showdown/sim/teams.ts'],
|
||||
'play.pokemonshowdown.com/js/server/',
|
||||
compileOpts
|
||||
);
|
||||
|
|
@ -104,6 +104,8 @@ compiledFiles += compiler.compileToDir(`play.pokemonshowdown.com/src`, `play.pok
|
|||
|
||||
compiledFiles += compiler.compileToDir(`replay.pokemonshowdown.com/src`, `replay.pokemonshowdown.com/js`, compileOpts);
|
||||
|
||||
compiledFiles += compiler.compileToDir(`teams.pokemonshowdown.com/src`, `teams.pokemonshowdown.com/js`, compileOpts);
|
||||
|
||||
compiledFiles += compiler.compileToFile(
|
||||
[
|
||||
'play.pokemonshowdown.com/src/battle-dex.ts',
|
||||
|
|
@ -145,6 +147,7 @@ function addCachebuster(_, attr, url, urlQuery) {
|
|||
url = url.replace('/dex.pokemonshowdown.com/', '/' + routes.dex + '/');
|
||||
url = url.replace('/play.pokemonshowdown.com/', '/' + routes.client + '/');
|
||||
url = url.replace('/pokemonshowdown.com/', '/' + routes.root + '/');
|
||||
url = url.replace('/teams.pokemonshowdown.com/', '/' + routes.teams + '/');
|
||||
|
||||
if (urlQuery) {
|
||||
if (url.startsWith('/')) {
|
||||
|
|
@ -157,12 +160,15 @@ function addCachebuster(_, attr, url, urlQuery) {
|
|||
|
||||
return attr + '="' + url + '?' + hash + '"';
|
||||
} else {
|
||||
// hardcoded to Replays rn; TODO: generalize
|
||||
// TODO: generalize better
|
||||
let hash;
|
||||
try {
|
||||
const fstr = fs.readFileSync('replay.pokemonshowdown.com/' + url, UTF8);
|
||||
hash = crypto.createHash('md5').update(fstr).digest('hex').substr(0, 8);
|
||||
} catch {}
|
||||
for (const subdir of ['teams', 'replays']) {
|
||||
if (hash) break;
|
||||
try {
|
||||
const fstr = fs.readFileSync(subdir + '.pokemonshowdown.com/' + url, UTF8);
|
||||
hash = crypto.createHash('md5').update(fstr).digest('hex').substr(0, 8);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
return attr + '="' + url + '?' + (hash || 'v1') + '"';
|
||||
}
|
||||
|
|
@ -226,4 +232,8 @@ let replaysContents = fs.readFileSync('replay.pokemonshowdown.com/index.template
|
|||
replaysContents = replaysContents.replace(URL_REGEX, addCachebuster);
|
||||
fs.writeFileSync('replay.pokemonshowdown.com/index.php', replaysContents);
|
||||
|
||||
let teamsContents = fs.readFileSync('teams.pokemonshowdown.com/index.template.html', UTF8);
|
||||
teamsContents = teamsContents.replace(URL_REGEX, addCachebuster);
|
||||
fs.writeFileSync('teams.pokemonshowdown.com/index.html', teamsContents);
|
||||
|
||||
console.log("DONE");
|
||||
|
|
|
|||
|
|
@ -3,5 +3,6 @@
|
|||
"client": "play.pokemonshowdown.com",
|
||||
"dex": "dex.pokemonshowdown.com",
|
||||
"replays": "replay.pokemonshowdown.com",
|
||||
"users": "pokemonshowdown.com/users"
|
||||
"users": "pokemonshowdown.com/users",
|
||||
"teams": "teams.pokemonshowdown.com"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,6 +105,8 @@ export default configure([
|
|||
'play.pokemonshowdown.com/src/*.tsx',
|
||||
'replay.pokemonshowdown.com/src/*.ts',
|
||||
'replay.pokemonshowdown.com/src/*.tsx',
|
||||
'teams.pokemonshowdown.com/src/*.ts',
|
||||
'teams.pokemonshowdown.com/src/*.tsx',
|
||||
],
|
||||
extends: [configs.es3ts],
|
||||
languageOptions: {
|
||||
|
|
|
|||
7
teams.pokemonshowdown.com/.htaccess
Normal file
7
teams.pokemonshowdown.com/.htaccess
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
RewriteEngine on
|
||||
RewriteRule ^api(/.*)?$ http://localhost:9000/api$1 [P,L]
|
||||
|
||||
RewriteRule ^([A-Za-z0-9-/]+)$ index.html [L,QSA]
|
||||
|
||||
DirectoryIndex index.php index.html /dirindex/dirindex.php
|
||||
ErrorDocument 404 /dirindex/404.html
|
||||
72
teams.pokemonshowdown.com/404.html
Normal file
72
teams.pokemonshowdown.com/404.html
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<title>Team Not Found - Pokémon Showdown!</title>
|
||||
|
||||
<link rel="stylesheet" href="//pokemonshowdown.com/style/global.css?v16" />
|
||||
<link rel="stylesheet" href="//play.pokemonshowdown.com/style/font-awesome.css?932f42c7" />
|
||||
|
||||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-26211653-1"></script>
|
||||
<script>
|
||||
window.dataLayer = window.dataLayer || [];
|
||||
function gtag(){dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'UA-26211653-1');
|
||||
</script>
|
||||
<!-- End Google Analytics -->
|
||||
<!-- Venatus Ad Manager - Install in <HEAD> of page -->
|
||||
<script src="https://hb.vntsm.com/v3/live/ad-manager.min.js" type="text/javascript" data-site-id="642aba63ec9a7b11c3c9c1be" data-mode="scan" async></script>
|
||||
<!-- / Venatus Ad Manager -->
|
||||
|
||||
<div class="body">
|
||||
|
||||
<header>
|
||||
<div class="nav-wrapper"><ul class="nav">
|
||||
<li><a class="button nav-first" href="//pokemonshowdown.com/"><img src="//pokemonshowdown.com/images/pokemonshowdownbeta.png" srcset="//pokemonshowdown.com/images/pokemonshowdownbeta.png 1x, //pokemonshowdown.com/images/pokemonshowdownbeta@2x.png 2x" alt="Pokémon Showdown" width="146" height="44" /> Home</a></li>
|
||||
<li><a class="button" href="//pokemonshowdown.com/dex/">Pokédex</a></li>
|
||||
<li><a class="button cur" href="/">Replay</a></li>
|
||||
<li><a class="button purplebutton" href="//smogon.com/dex/" target="_blank">Strategy</a></li>
|
||||
<li><a class="button nav-last purplebutton" href="//smogon.com/forums/" target="_blank">Forum</a></li>
|
||||
<li><a class="button greenbutton nav-first nav-last" href="//play.pokemonshowdown.com/">Play</a></li>
|
||||
</ul></div>
|
||||
</header>
|
||||
|
||||
<div class="main">
|
||||
|
||||
<section class="section" style="max-width:200px;margin:20px auto">
|
||||
<div style="text-align:center">
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style="image-rendering: pixelated"
|
||||
/><img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style="image-rendering: pixelated"
|
||||
/><img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-t.gif" alt="" style="image-rendering: pixelated" />
|
||||
</div>
|
||||
<div style="text-align:center">
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-f.gif" alt="" style="image-rendering: pixelated"
|
||||
/><img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style="image-rendering: pixelated"
|
||||
/><img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-u.gif" alt="" style="image-rendering: pixelated"
|
||||
/><img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style="image-rendering: pixelated"
|
||||
/><img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-d.gif" alt="" style="image-rendering: pixelated" />
|
||||
</div>
|
||||
</section><section class="section">
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
The team you're looking for is unavailable - either nonexistent or hidden. Check the URL and password?
|
||||
</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if (window.matchMedia) {
|
||||
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
document.documentElement.className = 'dark';
|
||||
}
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function (event) {
|
||||
document.documentElement.className = event.matches ? "dark" : "";
|
||||
});
|
||||
}
|
||||
</script>
|
||||
5
teams.pokemonshowdown.com/README.md
Normal file
5
teams.pokemonshowdown.com/README.md
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
PS teams viewer
|
||||
===================
|
||||
|
||||
This is the code powering https://teams.pokemonshowdown.com/
|
||||
|
||||
BIN
teams.pokemonshowdown.com/apple-touch-icon.png
Normal file
BIN
teams.pokemonshowdown.com/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
BIN
teams.pokemonshowdown.com/favicon.ico
Normal file
BIN
teams.pokemonshowdown.com/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
219
teams.pokemonshowdown.com/index.template.html
Normal file
219
teams.pokemonshowdown.com/index.template.html
Normal file
|
|
@ -0,0 +1,219 @@
|
|||
<!DOCTYPE html>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
|
||||
<title>Teams - Pokémon Showdown!</title>
|
||||
|
||||
<link rel="stylesheet" href="//pokemonshowdown.com/style/global.css" />
|
||||
<link rel="stylesheet" href="//play.pokemonshowdown.com/style/font-awesome.css" />
|
||||
<link rel="stylesheet" href="//play.pokemonshowdown.com/style/battle.css" />
|
||||
<link rel="stylesheet" href="//play.pokemonshowdown.com/style/utilichart.css" />
|
||||
|
||||
<style>
|
||||
@media (max-width:820px) {
|
||||
.battle {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.battle-log {
|
||||
margin: 7px auto 0;
|
||||
max-width: 640px;
|
||||
height: 300px;
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
.teamlist {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.teamlist li {
|
||||
margin: 3px 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.team {
|
||||
width: 230px;
|
||||
height: 32px;
|
||||
padding: 0 5px;
|
||||
font-size: 9pt;
|
||||
text-align: left;
|
||||
font-family: Verdana, Helvetica, Arial, sans-serif;
|
||||
cursor: pointer;
|
||||
|
||||
border-radius: 4px;
|
||||
border: 1px solid #888888;
|
||||
background: #EEEEEE;
|
||||
box-shadow: inset 0 1px 0 #FFFFFF;
|
||||
background: linear-gradient(to bottom, #edf2f8, #d7e3ec);
|
||||
color: black;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.teamlist .team {
|
||||
display: inline-block;
|
||||
white-space: nowrap;
|
||||
width: 350px;
|
||||
height: 49px;
|
||||
padding: 1px 6px 1px 6px;
|
||||
font-size: 8pt;
|
||||
vertical-align: middle;
|
||||
text-align: center;
|
||||
}
|
||||
.teamlist .dragging button {
|
||||
visibility: hidden;
|
||||
}
|
||||
.teamlist .dragging .team {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.teamlist .team small {
|
||||
display: block;
|
||||
padding: 0 45px 0 55px;
|
||||
}
|
||||
.teamlist .team .picon {
|
||||
margin: 0 -2px;
|
||||
}
|
||||
.teamlist .team .picon span {
|
||||
font-size: 9px !important;
|
||||
display: block;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
width: 32px;
|
||||
top: 22px;
|
||||
color: transparent;
|
||||
}
|
||||
.teamlist .team.pc-box {
|
||||
height: 139px;
|
||||
}
|
||||
|
||||
.optgroup {
|
||||
display: inline-block;
|
||||
line-height: 22px;
|
||||
font-size: 10pt;
|
||||
vertical-align: top;
|
||||
}
|
||||
.optgroup .button {
|
||||
height: 25px;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.optgroup button.button {
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
.linklist {
|
||||
list-style: none;
|
||||
margin: 0.5em 0;
|
||||
padding: 0;
|
||||
}
|
||||
.linklist li {
|
||||
padding: 2px 0;
|
||||
}
|
||||
.sidebar {
|
||||
float: left;
|
||||
width: 320px;
|
||||
}
|
||||
.bar-wrapper {
|
||||
max-width: 1100px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.bar-wrapper.has-sidebar {
|
||||
max-width: 1430px;
|
||||
}
|
||||
.mainbar {
|
||||
margin: 0;
|
||||
padding-right: 1px;
|
||||
}
|
||||
.mainbar.has-sidebar {
|
||||
margin-left: 330px;
|
||||
}
|
||||
@media (min-width: 1511px) {
|
||||
.sidebar {
|
||||
width: 400px;
|
||||
}
|
||||
.bar-wrapper.has-sidebar {
|
||||
max-width: 1510px;
|
||||
}
|
||||
.mainbar.has-sidebar {
|
||||
margin-left: 410px;
|
||||
}
|
||||
}
|
||||
.section.first-section {
|
||||
margin-top: 9px;
|
||||
}
|
||||
.blocklink small {
|
||||
white-space: normal;
|
||||
}
|
||||
.button {
|
||||
vertical-align: middle;
|
||||
}
|
||||
.replay-controls {
|
||||
padding-top: 10px;
|
||||
}
|
||||
.replay-controls h1 {
|
||||
font-size: 16pt;
|
||||
font-weight: normal;
|
||||
color: #CCC;
|
||||
}
|
||||
.pagelink {
|
||||
text-align: center;
|
||||
}
|
||||
.pagelink a {
|
||||
width: 150px;
|
||||
}
|
||||
.textbox, .button {
|
||||
font-size: 11pt;
|
||||
vertical-align: middle;
|
||||
}
|
||||
@media (max-width: 450px) {
|
||||
.button {
|
||||
font-size: 9pt;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
|
||||
<header>
|
||||
<div class="nav-wrapper"><ul class="nav">
|
||||
<li><a class="button nav-first" href="https://pokemonshowdown.com/"><img src="https://pokemonshowdown.com/images/pokemonshowdownbeta.png" srcset="https://pokemonshowdown.com/images/pokemonshowdownbeta.png 1x, https://pokemonshowdown.com/images/pokemonshowdownbeta@2x.png 2x" alt="Pokémon Showdown" width="146" height="44" /> Home</a></li>
|
||||
<li><a class="button" href="https://pokemonshowdown.com/dex/">Pokédex</a></li>
|
||||
<li><a class="button" href="#">Replay</a></li>
|
||||
<li><a class="button purplebutton" href="https://smogon.com/dex/" target="_blank">Strategy</a></li>
|
||||
<li><a class="button nav-last purplebutton" href="https://smogon.com/forums/" target="_blank">Forum</a></li>
|
||||
<li><a class="button greenbutton nav-first nav-last" href="https://play.pokemonshowdown.com/">Play</a></li>
|
||||
</ul></div>
|
||||
</header>
|
||||
|
||||
<div class="main" id="main">
|
||||
|
||||
<noscript><section class="section">You need to enable JavaScript to use this page; sorry!</section></noscript>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script nomodule src="//play.pokemonshowdown.com/js/lib/ps-polyfill.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/js/lib/preact.min.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/config/config.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/js/battledata.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/js/battle-dex-data.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/js/battle-dex.js?"></script>
|
||||
|
||||
<script src="//play.pokemonshowdown.com/data/pokedex.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/data/moves.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/data/items.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/data/abilities.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/data/pokedex-mini.js?"></script>
|
||||
|
||||
<script src="//play.pokemonshowdown.com/data/aliases.js?" async></script>
|
||||
|
||||
<script src="//play.pokemonshowdown.com/src/battle-log-misc.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/js/battle-log.js?"></script>
|
||||
<script src="//play.pokemonshowdown.com/js/battle.js?"></script>
|
||||
|
||||
<script src="//teams.pokemonshowdown.com/js/utils.js?"></script>
|
||||
<script> Net.defaultRoute = location.protocol + '//teams.pokemonshowdown.com'; </script>
|
||||
<script src="//teams.pokemonshowdown.com/js/teams-view.js?"></script>
|
||||
<script src="//teams.pokemonshowdown.com/js/teams-search.js?"></script>
|
||||
<script src="//teams.pokemonshowdown.com/js/teams-index.js?"></script>
|
||||
<script src="//teams.pokemonshowdown.com/js/teams.js?"></script>
|
||||
75
teams.pokemonshowdown.com/src/teams-index.tsx
Normal file
75
teams.pokemonshowdown.com/src/teams-index.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/** @jsx preact.h */
|
||||
/** @jsxFrag preact.Fragment */
|
||||
import preact from '../../play.pokemonshowdown.com/js/lib/preact';
|
||||
import { Net, MiniTeam, type ServerTeam } from './utils';
|
||||
import type { PageProps } from './teams';
|
||||
|
||||
declare const toID: (val: any) => string;
|
||||
|
||||
export class TeamIndex extends preact.Component<PageProps> {
|
||||
override state = {
|
||||
teams: [] as ServerTeam[],
|
||||
loggedIn: false as string | false,
|
||||
loading: true,
|
||||
search: null as string | null,
|
||||
};
|
||||
constructor(props: PageProps) {
|
||||
super(props);
|
||||
this.loadTeams();
|
||||
}
|
||||
loadTeams() {
|
||||
void Net('/api/getteams').get({ query: { full: 1 } }).then(resultText => {
|
||||
if (resultText.startsWith(']')) resultText = resultText.slice(1);
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(resultText);
|
||||
} catch {
|
||||
result = { actionerror: "Malformed response received. Try again later." };
|
||||
}
|
||||
this.setState({ ...result, loading: false });
|
||||
});
|
||||
}
|
||||
// todo: find proper type. preact docs unhelpful.
|
||||
onInput({ currentTarget }: any) {
|
||||
this.setState({ search: currentTarget.value });
|
||||
}
|
||||
searchMatch(team: ServerTeam) {
|
||||
const s = toID(this.state.search);
|
||||
if (!s) return true;
|
||||
if (!toID(this.state.search)) return true;
|
||||
if (toID(team.name).includes(s)) return true;
|
||||
if (toID(team.format).includes(s)) return true;
|
||||
if (team.team.split(',').map(toID).some(x => x.includes(s))) return true;
|
||||
if (`${team.teamid}`.startsWith(s)) return true;
|
||||
return false;
|
||||
}
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
return <div class="section" style={{ wordWrap: 'break-word' }}>Loading...</div>;
|
||||
}
|
||||
const teamsByFormat: Record<string, ServerTeam[]> = {};
|
||||
for (const team of this.state.teams) {
|
||||
// this way if a category is empty it doesn't just fill space, since it doesn't
|
||||
// get added unless it exists
|
||||
if (!this.searchMatch(team)) continue;
|
||||
if (!teamsByFormat[team.format]) teamsByFormat[team.format] = [];
|
||||
teamsByFormat[team.format].push(team);
|
||||
}
|
||||
return <div class="section" style={{ wordWrap: 'break-word' }}>
|
||||
<h2>Hi, {this.state.loggedIn || "guest"}!</h2>
|
||||
<label>Search all teams:</label> <a class="button" href="/search/">Go</a><br /><br />
|
||||
<label>Search your teams: </label>
|
||||
<input value={this.state.search || ""} onInput={e => this.onInput(e)} label="Search teams/formats"></input>
|
||||
<hr />
|
||||
{!this.state.teams.length ?
|
||||
<em>You have no teams lol</em> :
|
||||
Object.entries(teamsByFormat).map(([format, teams]) => (
|
||||
<><h4>{format}:</h4>
|
||||
<ul class="teamlist">{
|
||||
teams.map(team => <li><MiniTeam team={team} /></li>)
|
||||
}</ul>
|
||||
<hr /></>
|
||||
))}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
115
teams.pokemonshowdown.com/src/teams-search.tsx
Normal file
115
teams.pokemonshowdown.com/src/teams-search.tsx
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
/** @jsx preact.h */
|
||||
/** @jsxFrag preact.Fragment */
|
||||
import preact from '../../play.pokemonshowdown.com/js/lib/preact';
|
||||
import { Net, type ServerTeam, MiniTeam } from './utils';
|
||||
import type { PageProps } from './teams';
|
||||
|
||||
declare const toID: (val: any) => string;
|
||||
|
||||
const SEARCH_KEYS = ['format', 'owner', 'gen'];
|
||||
|
||||
export class TeamSearcher extends preact.Component<PageProps> {
|
||||
override state = {
|
||||
result: [] as ServerTeam[],
|
||||
curCount: 20,
|
||||
search: {} as Record<string, string>,
|
||||
loading: false,
|
||||
searchUnchanged: null as null | boolean,
|
||||
};
|
||||
constructor(props: PageProps) {
|
||||
super(props);
|
||||
|
||||
const url = new URL(location.href);
|
||||
let makeSearch = false;
|
||||
for (const key of SEARCH_KEYS) {
|
||||
const val = url.searchParams.get(key);
|
||||
if (toID(val)) {
|
||||
this.state.search[key] = toID(val);
|
||||
makeSearch = true;
|
||||
}
|
||||
if (this.props.args.type === key) { // prioritize url where applicable
|
||||
const propVal = toID(this.props.args.val);
|
||||
if (propVal) {
|
||||
this.state.search[key] = propVal;
|
||||
makeSearch = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
const count = Number(url.searchParams.get('count'));
|
||||
if (!isNaN(count) && count > 0) {
|
||||
this.state.curCount = count;
|
||||
}
|
||||
if (makeSearch) {
|
||||
this.search(0, true);
|
||||
}
|
||||
}
|
||||
// todo: find proper type. preact docs unhelpful.
|
||||
onInput(key: string, { currentTarget }: any) {
|
||||
this.state.search[key] = toID(currentTarget.value);
|
||||
this.setState({ search: this.state.search });
|
||||
}
|
||||
// format, owner, gen, count params. maxes out at 200. start at 20 and paginate
|
||||
search(incrementCount = 0, noSetUrl = false) {
|
||||
this.state.curCount += incrementCount;
|
||||
this.setState({
|
||||
loading: true,
|
||||
stateUnchanged: true,
|
||||
curCount: this.state.curCount,
|
||||
});
|
||||
|
||||
const url = new URL(location.href);
|
||||
// clear out old ones so they don't dupe
|
||||
for (const val in url.searchParams) url.searchParams.delete(val);
|
||||
// then set new ones
|
||||
for (const k in this.state.search) {
|
||||
url.searchParams.set(k, this.state.search[k]);
|
||||
}
|
||||
url.searchParams.set('count', `${this.state.curCount}`);
|
||||
if (!noSetUrl) history.pushState({}, '', url);
|
||||
|
||||
void Net('/api/searchteams').get({
|
||||
query: { ...this.state.search, count: this.state.curCount },
|
||||
}).then(resultText => {
|
||||
if (resultText.startsWith(']')) resultText = resultText.slice(1);
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(resultText);
|
||||
} catch {
|
||||
result = { actionerror: "Malformed response received. Try again later." };
|
||||
}
|
||||
this.setState({ ...result, loading: false });
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.loading) {
|
||||
return <div class="section" style={{ wordWrap: 'break-word' }}>Loading...</div>;
|
||||
}
|
||||
return <div class="section" style={{ wordWrap: 'break-word', textAlign: 'center' }}>
|
||||
<h1>Search Teams</h1>
|
||||
<br />
|
||||
<div name="searchsection">
|
||||
<label>Format: </label>
|
||||
<input value={this.state.search.format} onInput={e => this.onInput('format', e)} /><br />
|
||||
<label>Owner: </label>
|
||||
<input value={this.state.search.owner} onInput={e => this.onInput('owner', e)} /><br />
|
||||
<label>Generation: </label>
|
||||
<input value={this.state.search.gen} onInput={e => this.onInput('gen', e)} /><br />
|
||||
<button class="button notifying" onClick={() => this.search()}>Search!</button>
|
||||
</div>
|
||||
<hr />
|
||||
{!this.state.result.length ? <></> :
|
||||
<ul class="teamlist">{
|
||||
this.state.result.map(team => <li><MiniTeam team={team} fullTeam /></li>)
|
||||
}</ul>}
|
||||
{(this.state.result as any).actionerror ?
|
||||
<div class="message-error">{(this.state.result as any).actionerror}</div> :
|
||||
<></>}
|
||||
{
|
||||
this.state.result.length ?
|
||||
<button class="button notifying" onClick={() => this.search(20)}>More</button> :
|
||||
<></>
|
||||
}
|
||||
</div>;
|
||||
}
|
||||
}
|
||||
158
teams.pokemonshowdown.com/src/teams-view.tsx
Normal file
158
teams.pokemonshowdown.com/src/teams-view.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
/** @jsx preact.h */
|
||||
/** @jsxFrag preact.Fragment */
|
||||
import preact from '../../play.pokemonshowdown.com/js/lib/preact';
|
||||
import { Net, PSIcon, unpackTeam } from './utils';
|
||||
import { BattleLog } from '../../play.pokemonshowdown.com/src/battle-log';
|
||||
import type { PageProps } from './teams';
|
||||
import { Dex } from '../../play.pokemonshowdown.com/src/battle-dex';
|
||||
import { BattleStatNames } from '../../play.pokemonshowdown.com/src/battle-dex-data';
|
||||
|
||||
interface Team {
|
||||
team: string;
|
||||
title: string;
|
||||
views: number;
|
||||
ownerid: string;
|
||||
format: string;
|
||||
teamid: string;
|
||||
}
|
||||
|
||||
function PokemonSet({ set }: { set: Dex.PokemonSet }) {
|
||||
return <div>
|
||||
{set.name && set.name !== set.species ? <>{set.name} ({set.species})</> : <>{set.species}</>}
|
||||
{set.gender ? <> ({set.gender})</> : <></>}
|
||||
{set.item ? <> @ {set.item} </> : <></>}
|
||||
<br />
|
||||
{set.ability ? <>Ability: {set.ability}<br /></> : <></>}
|
||||
{set.level && set.level !== 100 ? <>Level: {set.level}<br /></> : <></>}
|
||||
{set.shiny ? <>Shiny: Yes<br /></> : <></>}
|
||||
{set.teraType ? <>Tera Type: {set.teraType}</> : <></>}
|
||||
|
||||
{set.evs ? <>{Dex.statNames.filter(stat => set.evs![stat]).map((stat, index, arr) => (
|
||||
<>
|
||||
{index === 0 ? 'EVs: ' : ''}
|
||||
{set.evs![stat]} {BattleStatNames[stat]}
|
||||
{index !== (arr.length - 1) ? ' / ' : ''}
|
||||
</>
|
||||
))}<br /></> : <></>}
|
||||
|
||||
{set.nature ? <>{set.nature} Nature<br /></> : <></>}
|
||||
|
||||
{set.ivs ? <>{Dex.statNames
|
||||
.filter(stat => !(set.ivs![stat] === undefined || isNaN(set.ivs![stat]) || set.ivs![stat] === 31))
|
||||
.map((stat, index, arr) =>
|
||||
<>
|
||||
{index === 0 ? 'IVs: ' : ''}
|
||||
{set.ivs![stat]} {BattleStatNames[stat]}
|
||||
{index !== (arr.length - 1) ? ' / ' : ''}
|
||||
</>
|
||||
)}<br /></> : <></>}
|
||||
|
||||
{set.moves ? set.moves.map(move => {
|
||||
if (move.substr(0, 13) === 'Hidden Power ') {
|
||||
const hpType = move.slice(13);
|
||||
move = move.slice(0, 13);
|
||||
move = `${move}[${hpType}]`;
|
||||
}
|
||||
// hide the alt so it doesn't interfere w/ copy/pasting
|
||||
return <>- {move} <PSIcon type={Dex.moves.get(move).type} hideAlt /><br /></>;
|
||||
}) : <></>}
|
||||
|
||||
{typeof set.happiness === 'number' && set.happiness !== 255 && !isNaN(set.happiness) ?
|
||||
<>Happiness: {set.happiness}<br /></> :
|
||||
<></>}
|
||||
{typeof set.dynamaxLevel === 'number' && set.dynamaxLevel !== 10 && !isNaN(set.dynamaxLevel) ?
|
||||
<>Dynamax Level: {set.dynamaxLevel}<br /></> :
|
||||
<></>}
|
||||
{set.gigantamax ? <>Gigantamax: Yes<br /></> : <></>}
|
||||
</div>;
|
||||
}
|
||||
|
||||
export class TeamViewer extends preact.Component<PageProps> {
|
||||
id: string;
|
||||
pw?: string;
|
||||
override state = {
|
||||
team: undefined as null | void | Team, error: undefined as string | undefined,
|
||||
};
|
||||
constructor(props: PageProps) {
|
||||
super(props);
|
||||
this.id = props.args.id;
|
||||
|
||||
this.checkTeamID();
|
||||
}
|
||||
render() {
|
||||
if (this.state.error) {
|
||||
return <div class="message-error">{this.state.error}</div>;
|
||||
}
|
||||
if (!this.state.team) {
|
||||
return <div class="section" style={{ textAlign: 'center' }}>{
|
||||
typeof this.state.team === 'undefined' ?
|
||||
JSON.stringify(this.state) :
|
||||
<>
|
||||
<h2 class="message-error">Team not found.</h2><br />
|
||||
<em>Either it doesn't exist or it's password protected. Check the link?</em>
|
||||
</>
|
||||
}</div>;
|
||||
}
|
||||
const { team, title, ownerid, format, views } = this.state.team;
|
||||
const teamData = unpackTeam(team);
|
||||
const gen = Number(/\d+/.exec(format)?.[0]) || 6;
|
||||
|
||||
return <div class="section" style={{ wordWrap: 'break-word' }}>
|
||||
<h2>{title}</h2>
|
||||
Owner: <strong style={{ color: BattleLog.usernameColor(ownerid as any) }}>{ownerid}</strong><br />
|
||||
Format: {format}<br />
|
||||
Views: {views}<br />
|
||||
<label>Shortlink: </label><a href={`https://psim.us/t/${this.id}`}>https://psim.us/t/{this.id}</a><br />
|
||||
<hr />
|
||||
<div name="sets" style={{ display: 'flex', flexWrap: 'wrap', rowGap: '1rem' }}>
|
||||
{teamData.map(
|
||||
set => <>
|
||||
<div style={{ flex: '0 0 20%' }}>
|
||||
<img src={
|
||||
Dex.getSpriteData(
|
||||
Dex.species.get(set.species),
|
||||
true,
|
||||
{ gen, shiny: set.shiny, gender: set.gender as 'F' }
|
||||
).url
|
||||
}
|
||||
/>
|
||||
{set.item ? <PSIcon item={set.item} /> : <></>}
|
||||
</div>
|
||||
<div style={{ flex: "0 0 80%", textAlign: 'left' }}>
|
||||
<PokemonSet set={set} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
checkTeamID() {
|
||||
if (this.id.includes('-')) {
|
||||
[this.id, this.pw] = this.id.split('-');
|
||||
}
|
||||
if (!/^\d+$/.test(this.id)) {
|
||||
this.setState({ error: "Invalid team ID: " + JSON.stringify(this.props.args) });
|
||||
return;
|
||||
}
|
||||
this.loadTeamData();
|
||||
}
|
||||
|
||||
loadTeamData() {
|
||||
void Net('/api/getteam').get({ query: { teamid: this.id, password: this.pw, full: 1 } }).then(resultText => {
|
||||
if (resultText.startsWith(']')) resultText = resultText.slice(1);
|
||||
let result;
|
||||
try {
|
||||
result = JSON.parse(resultText);
|
||||
} catch {
|
||||
result = { actionerror: "Malformed response received. Try again later." };
|
||||
}
|
||||
if (result.actionerror) {
|
||||
this.setState({ error: result.actionerror });
|
||||
} else {
|
||||
this.setState({ team: result.team === null ? result.team : result });
|
||||
}
|
||||
}).catch(e => {
|
||||
this.setState({ error: `HTTP${e.code}: ${e.message}` });
|
||||
});
|
||||
}
|
||||
}
|
||||
64
teams.pokemonshowdown.com/src/teams.tsx
Normal file
64
teams.pokemonshowdown.com/src/teams.tsx
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/** @jsx preact.h */
|
||||
import preact from '../../play.pokemonshowdown.com/js/lib/preact';
|
||||
import { TeamViewer } from './teams-view';
|
||||
import { TeamIndex } from './teams-index';
|
||||
import { TeamSearcher } from './teams-search';
|
||||
|
||||
export type PageProps = { args: Record<string, string> };
|
||||
|
||||
export const PSRouter = new class {
|
||||
routes: Record<string, (new (props: PageProps) => preact.Component<PageProps>)> = {};
|
||||
|
||||
setRoutes(routes: typeof PSRouter['routes']) {
|
||||
Object.assign(this.routes, routes);
|
||||
}
|
||||
|
||||
// @ts-expect-error 'no reachable end point' YES THERE IS changing href stops execution after it
|
||||
redir(path: string): never {
|
||||
location.href = path;
|
||||
}
|
||||
|
||||
go() {
|
||||
const params = location.pathname.split('/');
|
||||
let args: PageProps['args'] = {};
|
||||
let Element;
|
||||
for (const k in this.routes) {
|
||||
let matched = false;
|
||||
const routeParts = k.split('/');
|
||||
for (let i = 0; i < routeParts.length; i++) {
|
||||
const part = params[i];
|
||||
if (routeParts[i].startsWith('?')) {
|
||||
routeParts[i] = routeParts[i].slice(1);
|
||||
if (!part.trim()) break; // can end here
|
||||
}
|
||||
if (routeParts[i]?.startsWith(':')) {
|
||||
args[routeParts[i].slice(1)] = part;
|
||||
continue;
|
||||
}
|
||||
if (part !== routeParts[i]) {
|
||||
matched = false;
|
||||
args = {}; // don't accidentally dupe over args
|
||||
break;
|
||||
} else {
|
||||
matched = true;
|
||||
}
|
||||
}
|
||||
if (matched) {
|
||||
Element = this.routes[k];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!Element) {
|
||||
this.redir('//' + Config.routes.teams + "/404.html");
|
||||
} else {
|
||||
preact.render(<Element args={args} />, document.getElementById('main')!);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
PSRouter.setRoutes({
|
||||
'/view/:id': TeamViewer,
|
||||
'/': TeamIndex,
|
||||
'/search/?:type/?:val': TeamSearcher,
|
||||
});
|
||||
PSRouter.go();
|
||||
370
teams.pokemonshowdown.com/src/utils.tsx
Normal file
370
teams.pokemonshowdown.com/src/utils.tsx
Normal file
|
|
@ -0,0 +1,370 @@
|
|||
/**********************************************************************
|
||||
* Net
|
||||
*********************************************************************/
|
||||
/** @jsx preact.h */
|
||||
import preact from "../../play.pokemonshowdown.com/js/lib/preact";
|
||||
import { Dex } from "../../play.pokemonshowdown.com/src/battle-dex";
|
||||
|
||||
declare const toID: (str: any) => string;
|
||||
|
||||
export interface PostData {
|
||||
[key: string]: string | number | undefined;
|
||||
}
|
||||
export interface NetRequestOptions {
|
||||
method?: 'GET' | 'POST';
|
||||
body?: string | PostData;
|
||||
query?: PostData;
|
||||
}
|
||||
export class HttpError extends Error {
|
||||
statusCode?: number;
|
||||
body: string;
|
||||
constructor(message: string, statusCode: number | undefined, body: string) {
|
||||
super(message);
|
||||
this.name = 'HttpError';
|
||||
this.statusCode = statusCode;
|
||||
this.body = body;
|
||||
try {
|
||||
(Error as any).captureStackTrace(this, HttpError);
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
export class NetRequest {
|
||||
uri: string;
|
||||
constructor(uri: string) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a basic http/https request to the URI.
|
||||
* Returns the response data.
|
||||
*
|
||||
* Will throw if the response code isn't 200 OK.
|
||||
*
|
||||
* @param opts request opts
|
||||
*/
|
||||
get(opts: NetRequestOptions = {}): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
let uri = this.uri;
|
||||
if (opts.query) {
|
||||
uri += (uri.includes('?') ? '&' : '?') + Net.encodeQuery(opts.query);
|
||||
}
|
||||
xhr.open(opts.method || 'GET', uri);
|
||||
xhr.onreadystatechange = function () {
|
||||
const DONE = 4;
|
||||
if (xhr.readyState === DONE) {
|
||||
if (xhr.status === 200) {
|
||||
resolve(xhr.responseText || '');
|
||||
return;
|
||||
}
|
||||
const err = new HttpError(xhr.statusText || "Connection error", xhr.status, xhr.responseText);
|
||||
reject(err);
|
||||
}
|
||||
};
|
||||
if (opts.body) {
|
||||
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
|
||||
xhr.send(Net.encodeQuery(opts.body));
|
||||
} else {
|
||||
xhr.send();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a http/https POST request to the given link.
|
||||
* @param opts request opts
|
||||
* @param body POST body
|
||||
*/
|
||||
post(opts: Omit<NetRequestOptions, 'body'>, body: PostData | string): Promise<string>;
|
||||
/**
|
||||
* Makes a http/https POST request to the given link.
|
||||
* @param opts request opts
|
||||
*/
|
||||
post(opts?: NetRequestOptions): Promise<string>;
|
||||
post(opts: NetRequestOptions = {}, body?: PostData | string) {
|
||||
if (!body) body = opts.body;
|
||||
return this.get({
|
||||
...opts,
|
||||
method: 'POST',
|
||||
body,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function Net(uri: string) {
|
||||
if (uri.startsWith('/') && !uri.startsWith('//') && Net.defaultRoute) uri = Net.defaultRoute + uri;
|
||||
if (uri.startsWith('//') && document.location.protocol === 'file:') uri = 'https:' + uri;
|
||||
return new NetRequest(uri);
|
||||
}
|
||||
|
||||
/** Prepends URLs starting with `/` with this string. Used by testclient. */
|
||||
Net.defaultRoute = '';
|
||||
|
||||
Net.encodeQuery = function (data: string | PostData) {
|
||||
if (typeof data === 'string') return data;
|
||||
let urlencodedData = '';
|
||||
for (const key in data) {
|
||||
if ((data as any)[key] === undefined) continue;
|
||||
if (urlencodedData) urlencodedData += '&';
|
||||
urlencodedData += encodeURIComponent(key) + '=' + encodeURIComponent((data as any)[key]);
|
||||
}
|
||||
return urlencodedData;
|
||||
};
|
||||
Net.decodeQuery = function (query: string): { [key: string]: string } {
|
||||
let out: { [key: string]: string } = {};
|
||||
const questionIndex = query.indexOf('?');
|
||||
if (questionIndex >= 0) query = query.slice(questionIndex + 1);
|
||||
for (const queryPart of query.split('&')) {
|
||||
const [key, value] = queryPart.split('=');
|
||||
out[decodeURIComponent(key)] = decodeURIComponent(value || '');
|
||||
}
|
||||
return out;
|
||||
};
|
||||
|
||||
/**********************************************************************
|
||||
* Models
|
||||
*********************************************************************/
|
||||
|
||||
export class PSSubscription {
|
||||
observable: PSModel | PSStreamModel<any>;
|
||||
listener: (value?: any) => void;
|
||||
constructor(observable: PSModel | PSStreamModel<any>, listener: (value?: any) => void) {
|
||||
this.observable = observable;
|
||||
this.listener = listener;
|
||||
}
|
||||
unsubscribe() {
|
||||
const index = this.observable.subscriptions.indexOf(this);
|
||||
if (index >= 0) this.observable.subscriptions.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PS Models roughly implement the Observable spec. Not the entire
|
||||
* spec - just the parts we use. PSModel just notifies subscribers of
|
||||
* updates - a simple model for React.
|
||||
*/
|
||||
export class PSModel {
|
||||
subscriptions = [] as PSSubscription[];
|
||||
subscribe(listener: () => void) {
|
||||
const subscription = new PSSubscription(this, listener);
|
||||
this.subscriptions.push(subscription);
|
||||
return subscription;
|
||||
}
|
||||
subscribeAndRun(listener: () => void) {
|
||||
const subscription = this.subscribe(listener);
|
||||
subscription.listener();
|
||||
return subscription;
|
||||
}
|
||||
update() {
|
||||
for (const subscription of this.subscriptions) {
|
||||
subscription.listener();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PS Models roughly implement the Observable spec. PSStreamModel
|
||||
* streams some data out. This is very not-React, which generally
|
||||
* expects the DOM to be a pure function of state. Instead PSModels
|
||||
* which hold state, PSStreamModels give state directly to views,
|
||||
* so that the model doesn't need to hold a redundant copy of state.
|
||||
*/
|
||||
export class PSStreamModel<T = string> {
|
||||
subscriptions = [] as PSSubscription[];
|
||||
updates = [] as T[];
|
||||
subscribe(listener: (value: T) => void) {
|
||||
// TypeScript bug
|
||||
const subscription: PSSubscription = new PSSubscription(this, listener);
|
||||
this.subscriptions.push(subscription);
|
||||
if (this.updates.length) {
|
||||
for (const update of this.updates) {
|
||||
subscription.listener(update);
|
||||
}
|
||||
this.updates = [];
|
||||
}
|
||||
return subscription;
|
||||
}
|
||||
subscribeAndRun(listener: (value: T) => void) {
|
||||
const subscription = this.subscribe(listener);
|
||||
subscription.listener(null);
|
||||
return subscription;
|
||||
}
|
||||
update(value: T) {
|
||||
if (!this.subscriptions.length) {
|
||||
// save updates for later
|
||||
this.updates.push(value);
|
||||
}
|
||||
for (const subscription of this.subscriptions) {
|
||||
subscription.listener(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class PSIcon extends preact.Component<{
|
||||
pokemon?: string, item?: string, type?: string, category?: string, hideAlt?: boolean,
|
||||
}> {
|
||||
render() {
|
||||
if (this.props.pokemon) {
|
||||
return <span
|
||||
class="picon"
|
||||
style={{ background: Dex.getPokemonIcon(this.props.pokemon).replace('background:', '') }}
|
||||
></span>;
|
||||
} else if (this.props.item) {
|
||||
return <span
|
||||
className="itemicon"
|
||||
style={{ background: Dex.getItemIcon(this.props.item).replace('background:', '') }}
|
||||
></span>;
|
||||
} else if (this.props.type) {
|
||||
let type = Dex.types.get(this.props.type).name;
|
||||
if (!type) type = '???';
|
||||
let sanitizedType = type.replace(/\?/g, '%3f');
|
||||
return <img
|
||||
src={`${Dex.resourcePrefix}sprites/types/${sanitizedType}.png`}
|
||||
alt={this.props.hideAlt ? undefined : type}
|
||||
height="14"
|
||||
width="32"
|
||||
class="pixelated"
|
||||
/>;
|
||||
} else if (this.props.category) {
|
||||
const categoryID = toID(this.props.category);
|
||||
let sanitizedCategory = '';
|
||||
switch (categoryID) {
|
||||
case 'physical':
|
||||
case 'special':
|
||||
case 'status':
|
||||
sanitizedCategory = categoryID.charAt(0).toUpperCase() + categoryID.slice(1);
|
||||
break;
|
||||
default:
|
||||
sanitizedCategory = 'undefined';
|
||||
break;
|
||||
}
|
||||
return <img
|
||||
src={`${Dex.resourcePrefix}sprites/categories/${sanitizedCategory}.png`}
|
||||
alt={this.props.hideAlt ? undefined : sanitizedCategory}
|
||||
height="14"
|
||||
width="32"
|
||||
class="pixelated"
|
||||
/>;
|
||||
} else {
|
||||
return <span></span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type ServerTeam = {
|
||||
teamid: string,
|
||||
format: string,
|
||||
private: string,
|
||||
team: string,
|
||||
name?: string,
|
||||
title?: string,
|
||||
};
|
||||
|
||||
export function unpackTeam(buf: string) {
|
||||
if (!buf) return [];
|
||||
|
||||
let team: Dex.PokemonSet[] = [];
|
||||
|
||||
for (const setBuf of buf.split(`]`)) {
|
||||
const parts = setBuf.split(`|`);
|
||||
if (parts.length < 11) continue;
|
||||
let set: Dex.PokemonSet = { species: '', moves: [] };
|
||||
team.push(set);
|
||||
|
||||
// name
|
||||
set.name = parts[0];
|
||||
|
||||
// species
|
||||
set.species = Dex.species.get(parts[1]).name || set.name;
|
||||
|
||||
// item
|
||||
set.item = Dex.items.get(parts[2]).name;
|
||||
|
||||
// ability
|
||||
const species = Dex.species.get(set.species);
|
||||
set.ability =
|
||||
parts[3] === '-' ? '' :
|
||||
(species.baseSpecies === 'Zygarde' && parts[3] === 'H') ? 'Power Construct' :
|
||||
['', '0', '1', 'H', 'S'].includes(parts[3]) ?
|
||||
species.abilities[parts[3] as '0' || '0'] || (parts[3] === '' ? '' : '!!!ERROR!!!') :
|
||||
Dex.abilities.get(parts[3]).name;
|
||||
|
||||
// moves
|
||||
set.moves = parts[4].split(',').map(moveid =>
|
||||
Dex.moves.get(moveid).name
|
||||
);
|
||||
|
||||
// nature
|
||||
set.nature = parts[5] as Dex.NatureName;
|
||||
if (set.nature as any === 'undefined') set.nature = undefined;
|
||||
|
||||
// evs
|
||||
if (parts[6]) {
|
||||
if (parts[6].length > 5) {
|
||||
const evs = parts[6].split(',');
|
||||
set.evs = {
|
||||
hp: Number(evs[0]) || 0,
|
||||
atk: Number(evs[1]) || 0,
|
||||
def: Number(evs[2]) || 0,
|
||||
spa: Number(evs[3]) || 0,
|
||||
spd: Number(evs[4]) || 0,
|
||||
spe: Number(evs[5]) || 0,
|
||||
};
|
||||
} else if (parts[6] === '0') {
|
||||
set.evs = { hp: 0, atk: 0, def: 0, spa: 0, spd: 0, spe: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
// gender
|
||||
if (parts[7]) set.gender = parts[7];
|
||||
|
||||
// ivs
|
||||
if (parts[8]) {
|
||||
const ivs = parts[8].split(',');
|
||||
set.ivs = {
|
||||
hp: ivs[0] === '' ? 31 : Number(ivs[0]),
|
||||
atk: ivs[1] === '' ? 31 : Number(ivs[1]),
|
||||
def: ivs[2] === '' ? 31 : Number(ivs[2]),
|
||||
spa: ivs[3] === '' ? 31 : Number(ivs[3]),
|
||||
spd: ivs[4] === '' ? 31 : Number(ivs[4]),
|
||||
spe: ivs[5] === '' ? 31 : Number(ivs[5]),
|
||||
};
|
||||
}
|
||||
|
||||
// shiny
|
||||
if (parts[9]) set.shiny = true;
|
||||
|
||||
// level
|
||||
if (parts[10]) set.level = parseInt(parts[9], 10);
|
||||
|
||||
// happiness
|
||||
if (parts[11]) {
|
||||
let misc = parts[11].split(',', 4);
|
||||
set.happiness = (misc[0] ? Number(misc[0]) : undefined);
|
||||
set.hpType = misc[1];
|
||||
set.pokeball = misc[2];
|
||||
set.gigantamax = !!misc[3];
|
||||
set.dynamaxLevel = (misc[4] ? Number(misc[4]) : 10);
|
||||
}
|
||||
}
|
||||
|
||||
return team;
|
||||
}
|
||||
|
||||
export class MiniTeam extends preact.Component<{ team: ServerTeam, fullTeam?: boolean }> {
|
||||
render() {
|
||||
const team = this.props.team;
|
||||
return <>
|
||||
<a class="team" href={`/view/${team.teamid}${team.private ? `-${team.private}` : ''}`}>
|
||||
<strong>{team.name || team.title || "Untitled " + team.teamid}</strong>
|
||||
<br />
|
||||
<small>
|
||||
{(this.props.fullTeam ?
|
||||
unpackTeam(team.team).map(x => x.species) :
|
||||
team.team.split(',')
|
||||
).map(x => <PSIcon pokemon={x} />) || <em>(Empty team)</em>}
|
||||
</small>
|
||||
</a>
|
||||
</>;
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@
|
|||
"include": [
|
||||
"./play.pokemonshowdown.com/js/lib/preact.d.ts",
|
||||
"./play.pokemonshowdown.com/src/*",
|
||||
"./replay.pokemonshowdown.com/src/*"
|
||||
"./replay.pokemonshowdown.com/src/*",
|
||||
"./teams.pokemonshowdown.com/src/*"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user