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/caches/
|
||||||
/replay.pokemonshowdown.com/theme/wrapper.inc.php
|
/replay.pokemonshowdown.com/theme/wrapper.inc.php
|
||||||
/replay.pokemonshowdown.com/ads.txt
|
/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') {
|
if (process.argv[2] === 'full') {
|
||||||
delete compileOpts.ignore;
|
delete compileOpts.ignore;
|
||||||
compiler.compileToDir(
|
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/',
|
'play.pokemonshowdown.com/js/server/',
|
||||||
compileOpts
|
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(`replay.pokemonshowdown.com/src`, `replay.pokemonshowdown.com/js`, compileOpts);
|
||||||
|
|
||||||
|
compiledFiles += compiler.compileToDir(`teams.pokemonshowdown.com/src`, `teams.pokemonshowdown.com/js`, compileOpts);
|
||||||
|
|
||||||
compiledFiles += compiler.compileToFile(
|
compiledFiles += compiler.compileToFile(
|
||||||
[
|
[
|
||||||
'play.pokemonshowdown.com/src/battle-dex.ts',
|
'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('/dex.pokemonshowdown.com/', '/' + routes.dex + '/');
|
||||||
url = url.replace('/play.pokemonshowdown.com/', '/' + routes.client + '/');
|
url = url.replace('/play.pokemonshowdown.com/', '/' + routes.client + '/');
|
||||||
url = url.replace('/pokemonshowdown.com/', '/' + routes.root + '/');
|
url = url.replace('/pokemonshowdown.com/', '/' + routes.root + '/');
|
||||||
|
url = url.replace('/teams.pokemonshowdown.com/', '/' + routes.teams + '/');
|
||||||
|
|
||||||
if (urlQuery) {
|
if (urlQuery) {
|
||||||
if (url.startsWith('/')) {
|
if (url.startsWith('/')) {
|
||||||
|
|
@ -157,12 +160,15 @@ function addCachebuster(_, attr, url, urlQuery) {
|
||||||
|
|
||||||
return attr + '="' + url + '?' + hash + '"';
|
return attr + '="' + url + '?' + hash + '"';
|
||||||
} else {
|
} else {
|
||||||
// hardcoded to Replays rn; TODO: generalize
|
// TODO: generalize better
|
||||||
let hash;
|
let hash;
|
||||||
try {
|
for (const subdir of ['teams', 'replays']) {
|
||||||
const fstr = fs.readFileSync('replay.pokemonshowdown.com/' + url, UTF8);
|
if (hash) break;
|
||||||
hash = crypto.createHash('md5').update(fstr).digest('hex').substr(0, 8);
|
try {
|
||||||
} catch {}
|
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') + '"';
|
return attr + '="' + url + '?' + (hash || 'v1') + '"';
|
||||||
}
|
}
|
||||||
|
|
@ -226,4 +232,8 @@ let replaysContents = fs.readFileSync('replay.pokemonshowdown.com/index.template
|
||||||
replaysContents = replaysContents.replace(URL_REGEX, addCachebuster);
|
replaysContents = replaysContents.replace(URL_REGEX, addCachebuster);
|
||||||
fs.writeFileSync('replay.pokemonshowdown.com/index.php', replaysContents);
|
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");
|
console.log("DONE");
|
||||||
|
|
|
||||||
|
|
@ -3,5 +3,6 @@
|
||||||
"client": "play.pokemonshowdown.com",
|
"client": "play.pokemonshowdown.com",
|
||||||
"dex": "dex.pokemonshowdown.com",
|
"dex": "dex.pokemonshowdown.com",
|
||||||
"replays": "replay.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',
|
'play.pokemonshowdown.com/src/*.tsx',
|
||||||
'replay.pokemonshowdown.com/src/*.ts',
|
'replay.pokemonshowdown.com/src/*.ts',
|
||||||
'replay.pokemonshowdown.com/src/*.tsx',
|
'replay.pokemonshowdown.com/src/*.tsx',
|
||||||
|
'teams.pokemonshowdown.com/src/*.ts',
|
||||||
|
'teams.pokemonshowdown.com/src/*.tsx',
|
||||||
],
|
],
|
||||||
extends: [configs.es3ts],
|
extends: [configs.es3ts],
|
||||||
languageOptions: {
|
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": [
|
"include": [
|
||||||
"./play.pokemonshowdown.com/js/lib/preact.d.ts",
|
"./play.pokemonshowdown.com/js/lib/preact.d.ts",
|
||||||
"./play.pokemonshowdown.com/src/*",
|
"./play.pokemonshowdown.com/src/*",
|
||||||
"./replay.pokemonshowdown.com/src/*"
|
"./replay.pokemonshowdown.com/src/*",
|
||||||
|
"./teams.pokemonshowdown.com/src/*"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user