mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-04-22 23:40:34 -05:00
New Replays: Support pagination in search results
This commit is contained in:
parent
6d26acef8d
commit
0dcff6c2c3
|
|
@ -91,6 +91,20 @@
|
|||
.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;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div>
|
||||
|
|
@ -134,4 +148,5 @@
|
|||
<script defer src="//play.pokemonshowdown.com/js/battle.js?a7"></script>
|
||||
|
||||
<script defer src="js/utils.js?"></script>
|
||||
<script defer src="js/replays-battle.js?"></script>
|
||||
<script defer src="js/replays.js?"></script>
|
||||
|
|
|
|||
311
website/replays/src/replays-battle.tsx
Normal file
311
website/replays/src/replays-battle.tsx
Normal file
|
|
@ -0,0 +1,311 @@
|
|||
/** @jsx preact.h */
|
||||
import preact from 'preact';
|
||||
import $ from 'jquery';
|
||||
import {Net} from './utils';
|
||||
import {PSRouter} from './replays';
|
||||
import {Battle} from '../../../src/battle';
|
||||
import {BattleSound} from '../../../src/battle-sound';
|
||||
|
||||
function showAd(id: string) {
|
||||
// @ts-expect-error
|
||||
window.top.__vm_add = window.top.__vm_add || [];
|
||||
|
||||
//this is a x-browser way to make sure content has loaded.
|
||||
|
||||
(function (success) {
|
||||
if (window.document.readyState !== "loading") {
|
||||
success();
|
||||
} else {
|
||||
window.document.addEventListener("DOMContentLoaded", function () {
|
||||
success();
|
||||
});
|
||||
}
|
||||
})(function () {
|
||||
var placement = document.createElement("div");
|
||||
placement.setAttribute("class", "vm-placement");
|
||||
if (window.innerWidth > 1000) {
|
||||
//load desktop placement
|
||||
placement.setAttribute("data-id", "6452680c0b35755a3f09b59b");
|
||||
} else {
|
||||
//load mobile placement
|
||||
placement.setAttribute("data-id", "645268557bc7b571c2f06f62");
|
||||
}
|
||||
document.querySelector("#" + id)!.appendChild(placement);
|
||||
// @ts-expect-error
|
||||
window.top.__vm_add.push(placement);
|
||||
});
|
||||
}
|
||||
|
||||
export class BattleDiv extends preact.Component {
|
||||
override shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
override render() {
|
||||
return <div class="battle" style={{position: 'relative'}}></div>;
|
||||
}
|
||||
}
|
||||
class BattleLogDiv extends preact.Component {
|
||||
override shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
override render() {
|
||||
return <div class="battle-log"></div>;
|
||||
}
|
||||
}
|
||||
|
||||
export class BattlePanel extends preact.Component<{id: string}> {
|
||||
result: {
|
||||
uploadtime: number;
|
||||
id: string;
|
||||
format: string;
|
||||
p1: string;
|
||||
p2: string;
|
||||
log: string;
|
||||
views: number;
|
||||
p1id: string;
|
||||
p2id: string;
|
||||
rating: number;
|
||||
private: number;
|
||||
password: string;
|
||||
} | null | undefined = undefined;
|
||||
battle: Battle | null;
|
||||
speed = 'normal';
|
||||
override componentDidMount() {
|
||||
this.loadBattle(this.props.id);
|
||||
showAd('LeaderboardBTF');
|
||||
}
|
||||
override componentWillReceiveProps(nextProps) {
|
||||
if (this.stripQuery(this.props.id) !== this.stripQuery(nextProps.id)) {
|
||||
this.loadBattle(nextProps.id);
|
||||
}
|
||||
}
|
||||
stripQuery(id: string) {
|
||||
return id.includes('?') ? id.slice(0, id.indexOf('?')) : id;
|
||||
}
|
||||
loadBattle(id: string) {
|
||||
if (this.battle) this.battle.destroy();
|
||||
this.battle = null;
|
||||
this.result = undefined;
|
||||
|
||||
Net(`https://replay.pokemonshowdown.com/${this.stripQuery(id)}.json`).get().then(result => {
|
||||
const replay: NonNullable<BattlePanel['result']> = JSON.parse(result);
|
||||
this.result = replay;
|
||||
const $base = $(this.base!);
|
||||
this.battle = new Battle({
|
||||
id: replay.id,
|
||||
$frame: $base.find('.battle'),
|
||||
$logFrame: $base.find('.battle-log'),
|
||||
log: replay.log.split('\n'),
|
||||
isReplay: true,
|
||||
paused: true,
|
||||
autoresize: true,
|
||||
});
|
||||
// for ease of debugging
|
||||
(window as any).battle = this.battle;
|
||||
this.battle.subscribe(_ => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
if (id.includes('?p2')) {
|
||||
this.battle.switchViewpoint();
|
||||
}
|
||||
this.forceUpdate();
|
||||
}).catch(_ => {
|
||||
this.result = null;
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
override componentWillUnmount(): void {
|
||||
this.battle?.destroy();
|
||||
(window as any).battle = null;
|
||||
}
|
||||
play = () => {
|
||||
this.battle?.play();
|
||||
};
|
||||
replay = () => {
|
||||
this.battle?.reset();
|
||||
this.battle?.play();
|
||||
this.forceUpdate();
|
||||
};
|
||||
pause = () => {
|
||||
this.battle?.pause();
|
||||
};
|
||||
nextTurn = () => {
|
||||
this.battle?.seekBy(1);
|
||||
};
|
||||
prevTurn = () => {
|
||||
this.battle?.seekBy(-1);
|
||||
};
|
||||
firstTurn = () => {
|
||||
this.battle?.seekTurn(0);
|
||||
};
|
||||
lastTurn = () => {
|
||||
this.battle?.seekTurn(Infinity);
|
||||
};
|
||||
goToTurn = () => {
|
||||
const turn = prompt('Turn?');
|
||||
if (!turn?.trim()) return;
|
||||
let turnNum = Number(turn);
|
||||
if (turn === 'e' || turn === 'end' || turn === 'f' || turn === 'finish') turnNum = Infinity;
|
||||
if (isNaN(turnNum) || turnNum < 0) alert("Invalid turn");
|
||||
this.battle?.seekTurn(turnNum);
|
||||
};
|
||||
switchViewpoint = () => {
|
||||
this.battle?.switchViewpoint();
|
||||
if (this.battle?.viewpointSwitched) {
|
||||
PSRouter.replace(this.stripQuery(this.props.id) + '?p2');
|
||||
} else {
|
||||
PSRouter.replace(this.stripQuery(this.props.id));
|
||||
}
|
||||
};
|
||||
changeSpeed = (e: Event) => {
|
||||
this.speed = (e.target as HTMLSelectElement).value;
|
||||
const fadeTable = {
|
||||
hyperfast: 40,
|
||||
fast: 50,
|
||||
normal: 300,
|
||||
slow: 500,
|
||||
reallyslow: 1000
|
||||
};
|
||||
const delayTable = {
|
||||
hyperfast: 1,
|
||||
fast: 1,
|
||||
normal: 1,
|
||||
slow: 1000,
|
||||
reallyslow: 3000
|
||||
};
|
||||
if (!this.battle) return;
|
||||
this.battle.messageShownTime = delayTable[this.speed];
|
||||
this.battle.messageFadeTime = fadeTable[this.speed];
|
||||
this.battle.scene.updateAcceleration();
|
||||
};
|
||||
changeSound = (e: Event) => {
|
||||
const muted = (e.target as HTMLSelectElement).value;
|
||||
this.battle?.setMute(muted === 'off');
|
||||
};
|
||||
renderNotFound() {
|
||||
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'}><section class="section" style={{maxWidth: '200px'}}>
|
||||
{/* <div style={{margin: '0 -20px'}}>
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-n.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-o.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-t.png" alt="" width={120} height={120} />
|
||||
</div>
|
||||
<div style={{margin: '-40px -20px 0'}}>
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-f.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-o.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-u.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-n.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-d.png" alt="" width={120} height={120} />
|
||||
</div> */}
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-t.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
</div>
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-f.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-u.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-d.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
</div>
|
||||
</section><section class="section">
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
The battle you're looking for has expired. Battles expire after 15 minutes of inactivity unless they're saved.
|
||||
</p>
|
||||
<p>
|
||||
In the future, remember to click <strong>Upload and share replay</strong> to save a replay permanently.
|
||||
</p>
|
||||
</section></div>;
|
||||
}
|
||||
override render() {
|
||||
const atEnd = this.battle?.atQueueEnd;
|
||||
const atStart = !this.battle?.started;
|
||||
if (this.result === null) return this.renderNotFound();
|
||||
|
||||
let position: any = {};
|
||||
if (PSRouter.showingLeft()) {
|
||||
if (PSRouter.stickyRight) {
|
||||
position = {position: 'sticky', top: '0'};
|
||||
} else {
|
||||
position = {position: 'sticky', bottom: '0'};
|
||||
}
|
||||
}
|
||||
|
||||
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}><div style={{position: 'relative'}}>
|
||||
<BattleDiv />
|
||||
<BattleLogDiv />
|
||||
<div class="replay-controls">
|
||||
<p>
|
||||
{atEnd ?
|
||||
<button onClick={this.replay} class="button" style={{width: '5em'}}>
|
||||
<i class="fa fa-undo"></i><br />Replay
|
||||
</button>
|
||||
: this.battle?.paused ?
|
||||
<button onClick={this.play} class="button" style={{width: '5em'}}>
|
||||
<i class="fa fa-play"></i><br />Play
|
||||
</button>
|
||||
:
|
||||
<button onClick={this.pause} class="button" style={{width: '5em'}}>
|
||||
<i class="fa fa-pause"></i><br />Pause
|
||||
</button>
|
||||
} {}
|
||||
<button class={"button button-first" + (atStart ? " disabled" : "")} onClick={this.firstTurn}>
|
||||
<i class="fa fa-fast-backward"></i><br />First turn
|
||||
</button>
|
||||
<button class={"button button-middle" + (atStart ? " disabled" : "")} onClick={this.prevTurn}>
|
||||
<i class="fa fa-step-backward"></i><br />Prev turn
|
||||
</button>
|
||||
<button class={"button button-middle" + (atEnd ? " disabled" : "")} onClick={this.nextTurn}>
|
||||
<i class="fa fa-step-forward"></i><br />Skip turn
|
||||
</button>
|
||||
<button class={"button button-last" + (atEnd ? " disabled" : "")} onClick={this.lastTurn}>
|
||||
<i class="fa fa-fast-forward"></i><br />Skip to end
|
||||
</button> {}
|
||||
<button class="button" onClick={this.goToTurn}>
|
||||
<i class="fa fa-repeat"></i> Skip to turn...
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<label class="optgroup">
|
||||
Speed<br />
|
||||
<select name="speed" class="button" onChange={this.changeSpeed} value={this.speed}>
|
||||
<option value="hyperfast">Hyperfast</option>
|
||||
<option value="fast">Fast</option>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="slow">Slow</option>
|
||||
<option value="reallyslow">Really slow</option>
|
||||
</select>
|
||||
</label> {}
|
||||
<label class="optgroup">
|
||||
Sound<br />
|
||||
<select name="speed" class="button" onChange={this.changeSound} value={BattleSound.muted ? 'off' : 'on'}>
|
||||
<option value="on">On</option>
|
||||
<option value="off">Muted</option>
|
||||
</select>
|
||||
</label> {}
|
||||
<label class="optgroup">
|
||||
Viewpoint<br />
|
||||
<button onClick={this.switchViewpoint} class={this.battle ? 'button' : 'button disabled'}>
|
||||
{(this.battle?.viewpointSwitched ? this.result?.p2 : this.result?.p1)} {}
|
||||
<i class="fa fa-random" aria-label="Switch viewpoint"></i>
|
||||
</button>
|
||||
</label>
|
||||
</p>
|
||||
{this.result ? <h1>
|
||||
<strong>{this.result.format}</strong>: {this.result.p1} vs. {this.result.p2}
|
||||
</h1> : <h1>
|
||||
<em>Loading...</em>
|
||||
</h1>}
|
||||
{this.result ? <p>
|
||||
{this.result.uploadtime ? new Date(this.result.uploadtime * 1000).toDateString() : "Unknown upload date"}
|
||||
{this.result.rating ? ` | Rating: ${this.result.rating}` : ''}
|
||||
</p> : <p> </p>}
|
||||
{!PSRouter.showingLeft() && <p>
|
||||
<a href="." class="button"><i class="fa fa-caret-left"></i> More replays</a>
|
||||
</p>}
|
||||
</div>
|
||||
<div id="LeaderboardBTF"></div>
|
||||
</div></div>;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +1,9 @@
|
|||
/** @jsx preact.h */
|
||||
import preact from 'preact';
|
||||
import {Net, PSModel} from './utils';
|
||||
import {Battle} from '../../../src/battle';
|
||||
import {BattleSound} from '../../../src/battle-sound';
|
||||
import $ from 'jquery';
|
||||
import {BattlePanel} from './replays-battle';
|
||||
declare function toID(input: string): string;
|
||||
|
||||
function showAd(id: string) {
|
||||
// @ts-expect-error
|
||||
window.top.__vm_add = window.top.__vm_add || [];
|
||||
|
||||
//this is a x-browser way to make sure content has loaded.
|
||||
|
||||
(function (success) {
|
||||
if (window.document.readyState !== "loading") {
|
||||
success();
|
||||
} else {
|
||||
window.document.addEventListener("DOMContentLoaded", function () {
|
||||
success();
|
||||
});
|
||||
}
|
||||
})(function () {
|
||||
var placement = document.createElement("div");
|
||||
placement.setAttribute("class", "vm-placement");
|
||||
if (window.innerWidth > 1000) {
|
||||
//load desktop placement
|
||||
placement.setAttribute("data-id", "6452680c0b35755a3f09b59b");
|
||||
} else {
|
||||
//load mobile placement
|
||||
placement.setAttribute("data-id", "645268557bc7b571c2f06f62");
|
||||
}
|
||||
document.querySelector("#" + id)!.appendChild(placement);
|
||||
// @ts-expect-error
|
||||
window.top.__vm_add.push(placement);
|
||||
});
|
||||
}
|
||||
|
||||
interface ReplayResult {
|
||||
uploadtime: number;
|
||||
id: string;
|
||||
|
|
@ -50,6 +18,8 @@ class SearchPanel extends preact.Component {
|
|||
resultError: string | null = null;
|
||||
format = '';
|
||||
user = '';
|
||||
isPrivate = false;
|
||||
page = 1;
|
||||
loggedInUser: string | null = null;
|
||||
loggedInUserIsSysop = false;
|
||||
sort = 'date';
|
||||
|
|
@ -61,10 +31,18 @@ class SearchPanel extends preact.Component {
|
|||
this.loggedInUserIsSysop = !!sysop;
|
||||
this.forceUpdate();
|
||||
});
|
||||
this.updateSearch();
|
||||
}
|
||||
override componentDidUpdate() {
|
||||
const page = parseInt(decodeURIComponent(/\bpage=([^&]*)/.exec(PSRouter.leftLoc || '')?.[1] || '1'));
|
||||
if (page !== this.page) this.updateSearch();
|
||||
}
|
||||
updateSearch() {
|
||||
const user = decodeURIComponent(/\buser=([^&]*)/.exec(PSRouter.leftLoc || '')?.[1] || '');
|
||||
const format = decodeURIComponent(/\bformat=([^&]*)/.exec(PSRouter.leftLoc || '')?.[1] || '');
|
||||
const page = parseInt(decodeURIComponent(/\bpage=([^&]*)/.exec(PSRouter.leftLoc || '')?.[1] || '1'));
|
||||
const isPrivate = PSRouter.leftLoc!.includes('private=1');
|
||||
this.search(user, format, isPrivate);
|
||||
this.search(user, format, isPrivate, page);
|
||||
}
|
||||
parseResponse(response: string, isPrivate?: boolean) {
|
||||
this.results = null;
|
||||
|
|
@ -84,7 +62,7 @@ class SearchPanel extends preact.Component {
|
|||
}
|
||||
this.results = results;
|
||||
}
|
||||
search(user: string, format: string, isPrivate?: boolean) {
|
||||
search(user: string, format: string, isPrivate?: boolean, page = 1) {
|
||||
this.base!.querySelector<HTMLInputElement>('input[name=user]')!.value = user;
|
||||
this.base!.querySelector<HTMLInputElement>('input[name=format]')!.value = format;
|
||||
this.base!.querySelectorAll<HTMLInputElement>('input[name=private]')[isPrivate ? 1 : 0]!.checked = true;
|
||||
|
|
@ -92,6 +70,8 @@ class SearchPanel extends preact.Component {
|
|||
if (!format && !user) return this.recent();
|
||||
this.user = user;
|
||||
this.format = format;
|
||||
this.isPrivate = !!isPrivate;
|
||||
this.page = page;
|
||||
this.results = null;
|
||||
this.resultError = null;
|
||||
|
||||
|
|
@ -102,11 +82,12 @@ class SearchPanel extends preact.Component {
|
|||
user: user || undefined,
|
||||
format: format || undefined,
|
||||
private: isPrivate ? '1' : undefined,
|
||||
page: page === 1 ? undefined : page,
|
||||
}));
|
||||
}
|
||||
this.forceUpdate();
|
||||
Net(`/api/replays/${isPrivate ? 'searchprivate' : 'search'}`).get({
|
||||
query: {username: this.user, format: this.format},
|
||||
query: {username: this.user, format: this.format, page},
|
||||
}).then(response => {
|
||||
if (this.format !== format || this.user !== user) return;
|
||||
this.parseResponse(response, true);
|
||||
|
|
@ -117,6 +98,22 @@ class SearchPanel extends preact.Component {
|
|||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
prevPageLink() {
|
||||
return './?' + Net.encodeQuery({
|
||||
user: this.user || undefined,
|
||||
format: this.format || undefined,
|
||||
private: this.isPrivate ? '1' : undefined,
|
||||
page: this.page - 1 === 1 ? undefined : this.page - 1,
|
||||
});
|
||||
}
|
||||
nextPageLink() {
|
||||
return './?' + Net.encodeQuery({
|
||||
user: this.user || undefined,
|
||||
format: this.format || undefined,
|
||||
private: this.isPrivate ? '1' : undefined,
|
||||
page: this.page + 1,
|
||||
});
|
||||
}
|
||||
recent() {
|
||||
this.format = '';
|
||||
this.user = '';
|
||||
|
|
@ -165,21 +162,23 @@ class SearchPanel extends preact.Component {
|
|||
return formatid;
|
||||
}
|
||||
override render() {
|
||||
const activelySearching = !!(this.format || this.user);
|
||||
const hasNextPageLink = (this.results?.length || 0) > 50;
|
||||
const results = hasNextPageLink ? this.results!.slice(0, 50) : this.results;
|
||||
const searchResults = <ul class="linklist">
|
||||
{(this.resultError && <li>
|
||||
<strong class="message-error">{this.resultError}</strong>
|
||||
</li>) ||
|
||||
(!this.results && <li>
|
||||
(!results && <li>
|
||||
<em>Loading...</em>
|
||||
</li>) ||
|
||||
(this.results?.map(result => <li>
|
||||
(results?.map(result => <li>
|
||||
<a href={this.url(result)} class="blocklink">
|
||||
<small>[{this.formatid(result)}]<br /></small>
|
||||
<strong>{result.p1}</strong> vs. <strong>{result.p2}</strong>
|
||||
</a>
|
||||
</li>))}
|
||||
</ul>;
|
||||
const activelySearching = !!(this.format || this.user);
|
||||
return <div class={PSRouter.showingRight() ? 'sidebar' : ''}><section class="section first-section">
|
||||
<h1>Search replays</h1>
|
||||
<form onSubmit={this.submitForm}>
|
||||
|
|
@ -201,7 +200,13 @@ class SearchPanel extends preact.Component {
|
|||
{activelySearching && <button class="button" onClick={this.cancelForm}>Cancel</button>}
|
||||
</p>
|
||||
{activelySearching && <h1 aria-label="Results"></h1>}
|
||||
{activelySearching && this.page > 1 && <p class="pagelink">
|
||||
<a href={this.prevPageLink()} class="button"><i class="fa fa-caret-up"></i><br />Page {this.page - 1}</a>
|
||||
</p>}
|
||||
{activelySearching && searchResults}
|
||||
{activelySearching && (this.results?.length || 0) > 50 && <p class="pagelink">
|
||||
<a href={this.nextPageLink()} class="button">Page {this.page + 1}<br /><i class="fa fa-caret-down"></i></a>
|
||||
</p>}
|
||||
</form>
|
||||
</section>{!activelySearching && <FeaturedReplays />}{!activelySearching && <section class="section">
|
||||
<h1>Recent replays</h1>
|
||||
|
|
@ -322,280 +327,7 @@ class FeaturedReplays extends preact.Component {
|
|||
}
|
||||
}
|
||||
|
||||
class BattleDiv extends preact.Component {
|
||||
override shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
override render() {
|
||||
return <div class="battle" style={{position: 'relative'}}></div>;
|
||||
}
|
||||
}
|
||||
class BattleLogDiv extends preact.Component {
|
||||
override shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
override render() {
|
||||
return <div class="battle-log"></div>;
|
||||
}
|
||||
}
|
||||
|
||||
class BattlePanel extends preact.Component<{id: string}> {
|
||||
result: {
|
||||
uploadtime: number;
|
||||
id: string;
|
||||
format: string;
|
||||
p1: string;
|
||||
p2: string;
|
||||
log: string;
|
||||
views: number;
|
||||
p1id: string;
|
||||
p2id: string;
|
||||
rating: number;
|
||||
private: number;
|
||||
password: string;
|
||||
} | null | undefined = undefined;
|
||||
battle: Battle | null;
|
||||
speed = 'normal';
|
||||
override componentDidMount() {
|
||||
this.loadBattle(this.props.id);
|
||||
showAd('LeaderboardBTF');
|
||||
}
|
||||
override componentWillReceiveProps(nextProps) {
|
||||
if (this.stripQuery(this.props.id) !== this.stripQuery(nextProps.id)) {
|
||||
this.loadBattle(nextProps.id);
|
||||
}
|
||||
}
|
||||
stripQuery(id: string) {
|
||||
return id.includes('?') ? id.slice(0, id.indexOf('?')) : id;
|
||||
}
|
||||
loadBattle(id: string) {
|
||||
if (this.battle) this.battle.destroy();
|
||||
this.battle = null;
|
||||
this.result = undefined;
|
||||
|
||||
Net(`https://replay.pokemonshowdown.com/${this.stripQuery(id)}.json`).get().then(result => {
|
||||
const replay: NonNullable<BattlePanel['result']> = JSON.parse(result);
|
||||
this.result = replay;
|
||||
const $base = $(this.base!);
|
||||
this.battle = new Battle({
|
||||
id: replay.id,
|
||||
$frame: $base.find('.battle'),
|
||||
$logFrame: $base.find('.battle-log'),
|
||||
log: replay.log.split('\n'),
|
||||
isReplay: true,
|
||||
paused: true,
|
||||
autoresize: true,
|
||||
});
|
||||
// for ease of debugging
|
||||
(window as any).battle = this.battle;
|
||||
this.battle.subscribe(_ => {
|
||||
this.forceUpdate();
|
||||
});
|
||||
if (id.includes('?p2')) {
|
||||
this.battle.switchSides();
|
||||
}
|
||||
this.forceUpdate();
|
||||
}).catch(_ => {
|
||||
this.result = null;
|
||||
this.forceUpdate();
|
||||
});
|
||||
}
|
||||
override componentWillUnmount(): void {
|
||||
this.battle?.destroy();
|
||||
(window as any).battle = null;
|
||||
}
|
||||
play = () => {
|
||||
this.battle?.play();
|
||||
};
|
||||
replay = () => {
|
||||
this.battle?.reset();
|
||||
this.battle?.play();
|
||||
this.forceUpdate();
|
||||
};
|
||||
pause = () => {
|
||||
this.battle?.pause();
|
||||
};
|
||||
nextTurn = () => {
|
||||
this.battle?.seekBy(1);
|
||||
};
|
||||
prevTurn = () => {
|
||||
this.battle?.seekBy(-1);
|
||||
};
|
||||
firstTurn = () => {
|
||||
this.battle?.seekTurn(0);
|
||||
};
|
||||
lastTurn = () => {
|
||||
this.battle?.seekTurn(Infinity);
|
||||
};
|
||||
goToTurn = () => {
|
||||
const turn = prompt('Turn?');
|
||||
if (!turn?.trim()) return;
|
||||
let turnNum = Number(turn);
|
||||
if (turn === 'e' || turn === 'end' || turn === 'f' || turn === 'finish') turnNum = Infinity;
|
||||
if (isNaN(turnNum) || turnNum < 0) alert("Invalid turn");
|
||||
this.battle?.seekTurn(turnNum);
|
||||
};
|
||||
switchSides = () => {
|
||||
this.battle?.switchSides();
|
||||
if (this.battle?.sidesSwitched) {
|
||||
PSRouter.replace(this.stripQuery(this.props.id) + '?p2');
|
||||
} else {
|
||||
PSRouter.replace(this.stripQuery(this.props.id));
|
||||
}
|
||||
};
|
||||
changeSpeed = (e: Event) => {
|
||||
this.speed = (e.target as HTMLSelectElement).value;
|
||||
const fadeTable = {
|
||||
hyperfast: 40,
|
||||
fast: 50,
|
||||
normal: 300,
|
||||
slow: 500,
|
||||
reallyslow: 1000
|
||||
};
|
||||
const delayTable = {
|
||||
hyperfast: 1,
|
||||
fast: 1,
|
||||
normal: 1,
|
||||
slow: 1000,
|
||||
reallyslow: 3000
|
||||
};
|
||||
if (!this.battle) return;
|
||||
this.battle.messageShownTime = delayTable[this.speed];
|
||||
this.battle.messageFadeTime = fadeTable[this.speed];
|
||||
this.battle.scene.updateAcceleration();
|
||||
};
|
||||
changeSound = (e: Event) => {
|
||||
const muted = (e.target as HTMLSelectElement).value;
|
||||
this.battle?.setMute(muted === 'off');
|
||||
};
|
||||
renderNotFound() {
|
||||
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'}><section class="section" style={{maxWidth: '200px'}}>
|
||||
{/* <div style={{margin: '0 -20px'}}>
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-n.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-o.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-t.png" alt="" width={120} height={120} />
|
||||
</div>
|
||||
<div style={{margin: '-40px -20px 0'}}>
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-f.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-o.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-u.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-n.png" alt="" width={120} height={120} style={{marginRight: '-60px'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/dex/unown-d.png" alt="" width={120} height={120} />
|
||||
</div> */}
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-t.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
</div>
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-f.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-o.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-u.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-n.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
<img src="//play.pokemonshowdown.com/sprites/gen5ani/unown-d.gif" alt="" style={{imageRendering: 'pixelated'}} />
|
||||
</div>
|
||||
</section><section class="section">
|
||||
<h1>Not Found</h1>
|
||||
<p>
|
||||
The battle you're looking for has expired. Battles expire after 15 minutes of inactivity unless they're saved.
|
||||
</p>
|
||||
<p>
|
||||
In the future, remember to click <strong>Upload and share replay</strong> to save a replay permanently.
|
||||
</p>
|
||||
</section></div>;
|
||||
}
|
||||
override render() {
|
||||
const atEnd = this.battle?.atQueueEnd;
|
||||
const atStart = !this.battle?.started;
|
||||
if (this.result === null) return this.renderNotFound();
|
||||
|
||||
let position: any = {};
|
||||
if (PSRouter.showingLeft()) {
|
||||
if (PSRouter.stickyRight) {
|
||||
position = {position: 'sticky', top: '0'};
|
||||
} else {
|
||||
position = {position: 'sticky', bottom: '0'};
|
||||
}
|
||||
}
|
||||
|
||||
return <div class={PSRouter.showingLeft() ? 'mainbar has-sidebar' : 'mainbar'} style={position}><div style={{position: 'relative'}}>
|
||||
<BattleDiv />
|
||||
<BattleLogDiv />
|
||||
<div style={{paddingTop: 10}}>
|
||||
<p>
|
||||
{atEnd ?
|
||||
<button onClick={this.replay} class="button" style={{width: '5em'}}>
|
||||
<i class="fa fa-undo"></i><br />Replay
|
||||
</button>
|
||||
: this.battle?.paused ?
|
||||
<button onClick={this.play} class="button" style={{width: '5em'}}>
|
||||
<i class="fa fa-play"></i><br />Play
|
||||
</button>
|
||||
:
|
||||
<button onClick={this.pause} class="button" style={{width: '5em'}}>
|
||||
<i class="fa fa-pause"></i><br />Pause
|
||||
</button>
|
||||
} {}
|
||||
<button class={"button button-first" + (atStart ? " disabled" : "")} onClick={this.firstTurn}>
|
||||
<i class="fa fa-fast-backward"></i><br />First turn
|
||||
</button>
|
||||
<button class={"button button-middle" + (atStart ? " disabled" : "")} onClick={this.prevTurn}>
|
||||
<i class="fa fa-step-backward"></i><br />Prev turn
|
||||
</button>
|
||||
<button class={"button button-middle" + (atEnd ? " disabled" : "")} onClick={this.nextTurn}>
|
||||
<i class="fa fa-step-forward"></i><br />Skip turn
|
||||
</button>
|
||||
<button class={"button button-last" + (atEnd ? " disabled" : "")} onClick={this.lastTurn}>
|
||||
<i class="fa fa-fast-forward"></i><br />Skip to end
|
||||
</button> {}
|
||||
<button class="button" onClick={this.goToTurn}>
|
||||
<i class="fa fa-repeat"></i> Skip to turn...
|
||||
</button>
|
||||
</p>
|
||||
<p>
|
||||
<label class="optgroup">
|
||||
Speed<br />
|
||||
<select name="speed" class="button" onChange={this.changeSpeed} value={this.speed}>
|
||||
<option value="hyperfast">Hyperfast</option>
|
||||
<option value="fast">Fast</option>
|
||||
<option value="normal">Normal</option>
|
||||
<option value="slow">Slow</option>
|
||||
<option value="reallyslow">Really slow</option>
|
||||
</select>
|
||||
</label> {}
|
||||
<label class="optgroup">
|
||||
Sound<br />
|
||||
<select name="speed" class="button" onChange={this.changeSound} value={BattleSound.muted ? 'off' : 'on'}>
|
||||
<option value="on">On</option>
|
||||
<option value="off">Muted</option>
|
||||
</select>
|
||||
</label> {}
|
||||
<label class="optgroup">
|
||||
Viewpoint<br />
|
||||
<button onClick={this.switchSides} class={this.battle ? 'button' : 'button disabled'}>
|
||||
{(this.battle?.sidesSwitched ? this.result?.p2 : this.result?.p1)} <i class="fa fa-random"></i>
|
||||
</button>
|
||||
</label>
|
||||
</p>
|
||||
{this.result ? <h1 style={{fontWeight: 'normal', fontSize: '18pt'}}>
|
||||
<strong>{this.result.format}</strong>: {this.result.p1} vs. {this.result.p2}
|
||||
</h1> : <h1 style={{fontSize: '18pt'}}>
|
||||
<em>Loading...</em>
|
||||
</h1>}
|
||||
{this.result ? <p>
|
||||
{this.result.uploadtime ? new Date(this.result.uploadtime * 1000).toDateString() : "Unknown upload date"}
|
||||
{this.result.rating ? ` | Rating: ${this.result.rating}` : ''}
|
||||
</p> : <p> </p>}
|
||||
{!PSRouter.showingLeft() && <p>
|
||||
<a href="." class="button"><i class="fa fa-caret-left"></i> More replays</a>
|
||||
</p>}
|
||||
</div>
|
||||
<div id="LeaderboardBTF"></div>
|
||||
</div></div>;
|
||||
}
|
||||
}
|
||||
|
||||
const PSRouter = new class extends PSModel {
|
||||
export const PSRouter = new class extends PSModel {
|
||||
baseLoc: string;
|
||||
leftLoc: string | null = null;
|
||||
rightLoc: string | null = null;
|
||||
|
|
|
|||
|
|
@ -185,8 +185,12 @@ h1 {
|
|||
margin: 0;
|
||||
}
|
||||
.dark h1 {
|
||||
color: #CCC;
|
||||
border-bottom-color: #888;
|
||||
}
|
||||
.dark h2, .dark h3 {
|
||||
color: #CCC;
|
||||
}
|
||||
|
||||
.section {
|
||||
color: black;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user