mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-03-21 17:50:29 -05:00
Add support for watching battles
Playing battles is, of course, a lot harder, but watching them is at least implemented now.
This commit is contained in:
parent
6d415e2cd7
commit
9e06e72a35
|
|
@ -27,3 +27,4 @@ node_modules/
|
|||
/js/panel-teambuilder.js
|
||||
/js/panel-teambuilder-team.js
|
||||
/js/panel-teamdropdown.js
|
||||
/js/panel-battle.js
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -38,6 +38,7 @@ package-lock.json
|
|||
/js/panel-teambuilder.js
|
||||
/js/panel-teambuilder-team.js
|
||||
/js/panel-teamdropdown.js
|
||||
/js/panel-battle.js
|
||||
|
||||
/replays/caches/
|
||||
/replays/replay-config.inc.php
|
||||
|
|
|
|||
|
|
@ -68,6 +68,15 @@
|
|||
<script defer src="/js/battle-log.js?"></script>
|
||||
<script defer src="/js/panel-chat.js?"></script>
|
||||
|
||||
<script defer src="js/lib/soundmanager2-nodebug-jsmin.js"></script>
|
||||
<script defer src="js/lib/jquery-2.1.4.min.js"></script>
|
||||
<script defer src="data/graphics.js"></script>
|
||||
<script defer src="js/battle-text-parser.js"></script>
|
||||
<script defer src="data/text.js"></script>
|
||||
<script defer src="js/battle-tooltips.js"></script>
|
||||
<script defer src="js/battle.js"></script>
|
||||
<script defer src="js/panel-battle.js"></script>
|
||||
|
||||
<script defer src="/js/battle-dex-data.js?"></script>
|
||||
<script defer src="/data/pokedex.js?"></script>
|
||||
<script defer src="/data/moves.js?"></script>
|
||||
|
|
|
|||
|
|
@ -154,6 +154,7 @@ class BattleLog {
|
|||
|
||||
case 'seed': case 'choice': case ':': case 'timer':
|
||||
case 'J': case 'L': case 'N': case 'spectator': case 'spectatorleave':
|
||||
case 'initdone':
|
||||
return;
|
||||
|
||||
default:
|
||||
|
|
@ -190,12 +191,17 @@ class BattleLog {
|
|||
case 'turn':
|
||||
const h2elem = document.createElement('h2');
|
||||
h2elem.className = 'battle-history';
|
||||
let turnMessage = this.battleParser!.parseArgs(args, {}).trim();
|
||||
if (!turnMessage.startsWith('==') || !turnMessage.endsWith('==')) {
|
||||
throw new Error("Turn message must be a heading.");
|
||||
let turnMessage;
|
||||
if (this.battleParser) {
|
||||
turnMessage = this.battleParser.parseArgs(args, {}).trim();
|
||||
if (!turnMessage.startsWith('==') || !turnMessage.endsWith('==')) {
|
||||
throw new Error("Turn message must be a heading.");
|
||||
}
|
||||
turnMessage = turnMessage.slice(2, -2).trim();
|
||||
this.battleParser.curLineSection = 'break';
|
||||
} else {
|
||||
turnMessage = `Turn ${args[1]}`;
|
||||
}
|
||||
turnMessage = turnMessage.slice(2, -2).trim();
|
||||
this.battleParser!.curLineSection = 'break';
|
||||
h2elem.innerHTML = BattleLog.escapeHTML(turnMessage);
|
||||
this.addSpacer();
|
||||
this.addNode(h2elem);
|
||||
|
|
|
|||
|
|
@ -3482,7 +3482,11 @@ class Battle {
|
|||
else this.paused = false;
|
||||
this.fastForwardWillScroll = true;
|
||||
}
|
||||
if (!time) return;
|
||||
if (!time) {
|
||||
this.fastForwardOff();
|
||||
this.nextActivity();
|
||||
return;
|
||||
}
|
||||
this.scene.animationOff();
|
||||
this.playbackState = Playback.Seeking;
|
||||
this.fastForward = time;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ class PSConnection {
|
|||
socket: any = null;
|
||||
connected = false;
|
||||
queue = [] as string[];
|
||||
hostCheckInterval: number | undefined | null;
|
||||
constructor() {
|
||||
this.connect();
|
||||
}
|
||||
|
|
@ -26,7 +25,6 @@ class PSConnection {
|
|||
PS.connected = true;
|
||||
for (const msg of this.queue) socket.send(msg);
|
||||
this.queue = [];
|
||||
this.hostCheckInterval = setTimeout(() => this.doHostCheck(), 500);
|
||||
PS.update();
|
||||
};
|
||||
socket.onmessage = (e: MessageEvent) => {
|
||||
|
|
@ -34,10 +32,6 @@ class PSConnection {
|
|||
};
|
||||
socket.onclose = () => {
|
||||
console.log('\u2705 (DISCONNECTED)');
|
||||
if (this.hostCheckInterval !== null) {
|
||||
clearTimeout(this.hostCheckInterval);
|
||||
this.hostCheckInterval = null;
|
||||
}
|
||||
this.connected = false;
|
||||
PS.connected = false;
|
||||
PS.isOffline = true;
|
||||
|
|
@ -48,13 +42,6 @@ class PSConnection {
|
|||
PS.update();
|
||||
};
|
||||
}
|
||||
doHostCheck() {
|
||||
if (PS.server.host !== $.trim(PS.server.host)) {
|
||||
this.socket.close();
|
||||
} else {
|
||||
this.hostCheckInterval = setTimeout(() => this.doHostCheck(), 500);
|
||||
}
|
||||
}
|
||||
send(msg: string) {
|
||||
if (!this.connected) {
|
||||
this.queue.push(msg);
|
||||
|
|
|
|||
|
|
@ -648,8 +648,10 @@ const PS = new class extends PSModel {
|
|||
const roomid2 = roomid || 'lobby' as RoomID;
|
||||
let room = PS.rooms[roomid];
|
||||
console.log('\u2705 ' + (roomid ? '[' + roomid + '] ' : '') + '%c' + msg, "color: #007700");
|
||||
let isInit = false;
|
||||
for (const line of msg.split('\n')) {
|
||||
if (line.startsWith('|init|')) {
|
||||
isInit = true;
|
||||
room = PS.rooms[roomid2];
|
||||
const type = line.slice(6);
|
||||
if (!room) {
|
||||
|
|
@ -689,7 +691,7 @@ const PS = new class extends PSModel {
|
|||
}
|
||||
if (room) room.receive(line);
|
||||
}
|
||||
if (room) room.update(null);
|
||||
if (room) room.update(isInit ? `|initdone` : null);
|
||||
}
|
||||
send(fullMsg: string) {
|
||||
const pipeIndex = fullMsg.indexOf('|');
|
||||
|
|
|
|||
148
src/panel-battle.tsx
Normal file
148
src/panel-battle.tsx
Normal file
|
|
@ -0,0 +1,148 @@
|
|||
/**
|
||||
* Battle panel
|
||||
*
|
||||
* @author Guangcong Luo <guangcongluo@gmail.com>
|
||||
* @license AGPLv3
|
||||
*/
|
||||
|
||||
class BattleRoom extends ChatRoom {
|
||||
readonly classType = 'battle';
|
||||
// @ts-ignore assigned in parent constructor
|
||||
pmTarget: null;
|
||||
// @ts-ignore assigned in parent constructor
|
||||
challengeMenuOpen: false;
|
||||
// @ts-ignore assigned in parent constructor
|
||||
challengingFormat: null;
|
||||
// @ts-ignore assigned in parent constructor
|
||||
challengedFormat: null;
|
||||
battle: Battle = null!;
|
||||
/**
|
||||
* @return true to prevent line from being sent to server
|
||||
*/
|
||||
handleMessage(line: string) {
|
||||
if (!line.startsWith('/') || line.startsWith('//')) return false;
|
||||
const spaceIndex = line.indexOf(' ');
|
||||
const cmd = spaceIndex >= 0 ? line.slice(1, spaceIndex) : line.slice(1);
|
||||
const target = spaceIndex >= 0 ? line.slice(spaceIndex + 1) : '';
|
||||
switch (cmd) {
|
||||
case 'play': {
|
||||
this.battle.play();
|
||||
this.update('');
|
||||
return true;
|
||||
} case 'pause': {
|
||||
this.battle.pause();
|
||||
this.update('');
|
||||
return true;
|
||||
} case 'ffto': case 'fastfowardto': {
|
||||
let turnNum = Number(target);
|
||||
if (target.charAt(0) === '+' || turnNum < 0) {
|
||||
turnNum += this.battle.turn;
|
||||
if (turnNum < 0) turnNum = 0;
|
||||
} else if (target === 'end') {
|
||||
turnNum = -1;
|
||||
}
|
||||
if (isNaN(turnNum)) {
|
||||
this.receive(`|error|/ffto - Invalid turn number: ${target}`);
|
||||
return true;
|
||||
}
|
||||
this.battle.fastForwardTo(turnNum);
|
||||
this.update('');
|
||||
return true;
|
||||
} case 'switchsides': {
|
||||
this.battle.switchSides();
|
||||
return true;
|
||||
}}
|
||||
return super.handleMessage(line);
|
||||
}
|
||||
}
|
||||
|
||||
class BattleDiv extends preact.Component {
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
render() {
|
||||
return <div class="battle"></div>;
|
||||
}
|
||||
}
|
||||
|
||||
class BattlePanel extends PSRoomPanel<BattleRoom> {
|
||||
send = (text: string) => {
|
||||
this.props.room.send(text);
|
||||
};
|
||||
focus() {
|
||||
this.base!.querySelector('textarea')!.focus();
|
||||
}
|
||||
focusIfNoSelection = () => {
|
||||
const selection = window.getSelection()!;
|
||||
if (selection.type === 'Range') return;
|
||||
this.focus();
|
||||
};
|
||||
onKey = (e: KeyboardEvent) => {
|
||||
if (e.keyCode === 33) { // Pg Up key
|
||||
const chatLog = this.base!.getElementsByClassName('chat-log')[0] as HTMLDivElement;
|
||||
chatLog.scrollTop = chatLog.scrollTop - chatLog.offsetHeight + 60;
|
||||
return true;
|
||||
} else if (e.keyCode === 34) { // Pg Dn key
|
||||
const chatLog = this.base!.getElementsByClassName('chat-log')[0] as HTMLDivElement;
|
||||
chatLog.scrollTop = chatLog.scrollTop + chatLog.offsetHeight - 60;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
componentDidMount() {
|
||||
const battle = new Battle($(this.base!).find('.battle'), $(this.base!).find('.battle-log'));
|
||||
this.props.room.battle = battle;
|
||||
battle.endCallback = () => this.forceUpdate();
|
||||
battle.play();
|
||||
super.componentDidMount();
|
||||
}
|
||||
receive(line: string) {
|
||||
if (line === `|initdone`) {
|
||||
this.props.room.battle.fastForwardTo(-1);
|
||||
return;
|
||||
}
|
||||
this.props.room.battle.add(line);
|
||||
}
|
||||
renderControls() {
|
||||
const battle = this.props.room.battle;
|
||||
if (!battle) return null;
|
||||
const atEnd = battle.playbackState === Playback.Finished;
|
||||
return <div class="battle-controls" role="complementary" aria-label="Battle Controls" style="top: 370px;">
|
||||
<p>
|
||||
{atEnd ?
|
||||
<button class="button disabled" name="cmd" value="/play"><i class="fa fa-play"></i><br />Play</button>
|
||||
: battle.paused ?
|
||||
<button class="button" name="cmd" value="/play"><i class="fa fa-play"></i><br />Play</button>
|
||||
:
|
||||
<button class="button" name="cmd" value="/pause"><i class="fa fa-pause"></i><br />Pause</button>
|
||||
} {}
|
||||
<button class="button" name="cmd" value="/ffto -1"><i class="fa fa-step-backward"></i><br />Last turn</button>
|
||||
<button class={"button" + (atEnd ? " disabled" : "")} name="cmd" value="/ffto +1"><i class="fa fa-step-forward"></i><br />Skip turn</button> {}
|
||||
<button class="button" name="cmd" value="/ffto 0"><i class="fa fa-undo"></i><br />First turn</button>
|
||||
<button class={"button" + (atEnd ? " disabled" : "")} name="cmd" value="/ffto end"><i class="fa fa-fast-forward"></i><br />Skip to end</button>
|
||||
</p>
|
||||
<p>
|
||||
<button class="button" name="cmd" value="/switchsides"><i class="fa fa-random"></i> Switch sides</button>
|
||||
</p>
|
||||
</div>;
|
||||
}
|
||||
render() {
|
||||
const room = this.props.room;
|
||||
|
||||
return <PSPanelWrapper room={room}>
|
||||
<BattleDiv></BattleDiv>
|
||||
<ChatLog class="battle-log hasuserlist" room={this.props.room} onClick={this.focusIfNoSelection} left={640} noSubscription>
|
||||
{}
|
||||
</ChatLog>
|
||||
<ChatTextEntry room={this.props.room} onMessage={this.send} onKey={this.onKey} left={640} />
|
||||
<ChatUserList room={this.props.room} left={640} minimized />
|
||||
{this.renderControls()}
|
||||
</PSPanelWrapper>;
|
||||
}
|
||||
}
|
||||
|
||||
PS.roomTypes['battle'] = {
|
||||
Model: BattleRoom,
|
||||
Component: BattlePanel,
|
||||
};
|
||||
PS.updateRoomTypes();
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
class ChatRoom extends PSRoom {
|
||||
readonly classType: string = 'chat';
|
||||
readonly classType: 'chat' | 'battle' = 'chat';
|
||||
users: {[userid: string]: string} = {};
|
||||
userCount = 0;
|
||||
|
||||
|
|
@ -152,6 +152,7 @@ class ChatRoom extends PSRoom {
|
|||
|
||||
class ChatTextEntry extends preact.Component<{
|
||||
room: PSRoom, onMessage: (msg: string) => void, onKey: (e: KeyboardEvent) => boolean,
|
||||
left?: number,
|
||||
}> {
|
||||
subscription: PSSubscription | null = null;
|
||||
textbox: HTMLTextAreaElement = null!;
|
||||
|
|
@ -295,7 +296,9 @@ class ChatTextEntry extends preact.Component<{
|
|||
return true;
|
||||
}
|
||||
render() {
|
||||
return <div class="chat-log-add hasuserlist" onClick={this.focusIfNoSelection}>
|
||||
return <div
|
||||
class="chat-log-add hasuserlist" onClick={this.focusIfNoSelection} style={{left: this.props.left || 0}}
|
||||
>
|
||||
<form class="chatbox">
|
||||
<label style={{color: BattleLog.usernameColor(PS.user.userid)}}>{PS.user.name}:</label>
|
||||
<textarea
|
||||
|
|
@ -376,18 +379,18 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
|
|||
</TeamForm>
|
||||
</div> : null;
|
||||
|
||||
return <PSPanelWrapper room={this.props.room}>
|
||||
return <PSPanelWrapper room={room}>
|
||||
<div class="tournament-wrapper hasuserlist"></div>
|
||||
<ChatLog class="chat-log hasuserlist" room={this.props.room} onClick={this.focusIfNoSelection}>
|
||||
<ChatLog class="chat-log" room={this.props.room} onClick={this.focusIfNoSelection} left={146}>
|
||||
{challengeTo || challengeFrom && [challengeTo, challengeFrom]}
|
||||
</ChatLog>
|
||||
<ChatTextEntry room={this.props.room} onMessage={this.send} onKey={this.onKey} />
|
||||
<ChatTextEntry room={this.props.room} onMessage={this.send} onKey={this.onKey} left={146} />
|
||||
<ChatUserList room={this.props.room} />
|
||||
</PSPanelWrapper>;
|
||||
}
|
||||
}
|
||||
|
||||
class ChatUserList extends preact.Component<{room: ChatRoom}> {
|
||||
class ChatUserList extends preact.Component<{room: ChatRoom, left?: number, minimized?: boolean}> {
|
||||
subscription: PSSubscription | null = null;
|
||||
componentDidMount() {
|
||||
this.subscription = this.props.room.subscribe(msg => {
|
||||
|
|
@ -404,7 +407,7 @@ class ChatUserList extends preact.Component<{room: ChatRoom}> {
|
|||
function colorStyle(userid: ID) {
|
||||
return {color: BattleLog.usernameColor(userid)};
|
||||
}
|
||||
return <ul class="userlist">
|
||||
return <ul class={'userlist' + (this.props.minimized ? ' userlist-minimized' : '')} style={{left: this.props.left || 0}}>
|
||||
<li class="userlist-count" style="text-align:center;padding:2px 0"><small>{room.userCount} users</small></li>
|
||||
{userList.map(([userid, name]) => {
|
||||
const groupSymbol = name.charAt(0);
|
||||
|
|
@ -442,11 +445,14 @@ class ChatUserList extends preact.Component<{room: ChatRoom}> {
|
|||
|
||||
class ChatLog extends preact.Component<{
|
||||
class: string, room: ChatRoom, onClick?: (e: Event) => void, children?: preact.ComponentChildren,
|
||||
left?: number, top?: number, noSubscription?: boolean;
|
||||
}> {
|
||||
log: BattleLog = null!;
|
||||
log: BattleLog | null = null;
|
||||
subscription: PSSubscription | null = null;
|
||||
componentDidMount() {
|
||||
this.log = new BattleLog(this.base! as HTMLDivElement);
|
||||
if (!this.props.noSubscription) {
|
||||
this.log = new BattleLog(this.base! as HTMLDivElement);
|
||||
}
|
||||
this.subscription = this.props.room.subscribe(msg => {
|
||||
if (!msg) return;
|
||||
const tokens = PS.lineParse(msg);
|
||||
|
|
@ -470,7 +476,7 @@ class ChatLog extends preact.Component<{
|
|||
this.props.room.renameUser(tokens[1], tokens[2]);
|
||||
break;
|
||||
}
|
||||
this.log.add(tokens);
|
||||
if (!this.props.noSubscription) this.log!.add(tokens);
|
||||
});
|
||||
this.setControlsJSX(this.props.children);
|
||||
}
|
||||
|
|
@ -481,25 +487,37 @@ class ChatLog extends preact.Component<{
|
|||
if (props.class !== this.props.class) {
|
||||
this.base!.className = props.class;
|
||||
}
|
||||
if (props.left !== this.props.left) this.base!.style.left = `${props.left || 0}px`;
|
||||
if (props.top !== this.props.top) this.base!.style.top = `${props.top || 0}px`;
|
||||
this.setControlsJSX(props.children);
|
||||
this.log.updateScroll();
|
||||
this.updateScroll();
|
||||
return false;
|
||||
}
|
||||
setControlsJSX(jsx: preact.ComponentChildren | undefined) {
|
||||
const children = this.base!.children;
|
||||
let controlsElem: HTMLDivElement | undefined = children[children.length - 1] as HTMLDivElement;
|
||||
if (controlsElem.className !== 'controls') controlsElem = undefined;
|
||||
let controlsElem = children[children.length - 1] as HTMLDivElement | undefined;
|
||||
if (controlsElem && controlsElem.className !== 'controls') controlsElem = undefined;
|
||||
if (!jsx) {
|
||||
if (!controlsElem) return;
|
||||
preact.render(null, this.base!, controlsElem);
|
||||
this.log.updateScroll();
|
||||
this.updateScroll();
|
||||
return;
|
||||
}
|
||||
preact.render(<div class="controls">{jsx}</div>, this.base!, controlsElem);
|
||||
this.log.updateScroll();
|
||||
this.updateScroll();
|
||||
}
|
||||
updateScroll() {
|
||||
if (this.log) {
|
||||
this.log.updateScroll();
|
||||
} else if (this.props.room.battle) {
|
||||
this.log = (this.props.room.battle as Battle).scene.log;
|
||||
this.log.updateScroll();
|
||||
}
|
||||
}
|
||||
render() {
|
||||
return <div class={this.props.class} role="log" onClick={this.props.onClick}></div>;
|
||||
return <div class={this.props.class} role="log" onClick={this.props.onClick} style={{
|
||||
left: this.props.left || 0, top: this.props.top || 0,
|
||||
}}></div>;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class MainMenuRoom extends PSRoom {
|
|||
continue;
|
||||
}
|
||||
room.challengedFormat = json.challengesFrom[targetUserid] || null;
|
||||
room.challengingFormat = json.challengeTo.to === targetUserid ? json.challengeTo.format : null;
|
||||
room.challengingFormat = (json.challengeTo || {}).to === targetUserid ? json.challengeTo.format : null;
|
||||
room.update('');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1567,7 +1567,6 @@ a.ilink.yours {
|
|||
text-align: left;
|
||||
}
|
||||
.userlist li {
|
||||
border-bottom: 1px solid #CCCCCC;
|
||||
height: 19px;
|
||||
font: 10pt Verdana, sans-serif;
|
||||
white-space: nowrap;
|
||||
|
|
@ -1669,7 +1668,6 @@ a.ilink.yours {
|
|||
top: 370px;
|
||||
left: 0;
|
||||
width: 640px;
|
||||
background: #EEF2F5;
|
||||
}
|
||||
.battle-chat-toggle {
|
||||
display: none;
|
||||
|
|
@ -3180,8 +3178,7 @@ a.ilink.yours {
|
|||
color: #DDD;
|
||||
}
|
||||
.dark .ps-room.ps-room-opaque,
|
||||
.dark .battle-log-add,
|
||||
.dark .battle-controls {
|
||||
.dark .battle-log-add {
|
||||
background: #444444;
|
||||
color: #DDD;
|
||||
}
|
||||
|
|
@ -3301,9 +3298,6 @@ a.ilink.yours {
|
|||
.dark .userlist span {
|
||||
text-shadow: 1px 1px 0 #222222;
|
||||
}
|
||||
.dark .userlist li {
|
||||
border-bottom-color: #444;
|
||||
}
|
||||
.dark .userlist li button:hover {
|
||||
background: #222222;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,6 +69,15 @@
|
|||
<script src="js/battle-log.js"></script>
|
||||
<script src="js/panel-chat.js"></script>
|
||||
|
||||
<script src="js/lib/soundmanager2-nodebug-jsmin.js"></script>
|
||||
<script src="js/lib/jquery-2.1.4.min.js"></script>
|
||||
<script src="data/graphics.js"></script>
|
||||
<script src="js/battle-text-parser.js"></script>
|
||||
<script src="data/text.js"></script>
|
||||
<script src="js/battle-tooltips.js"></script>
|
||||
<script src="js/battle.js"></script>
|
||||
<script src="js/panel-battle.js"></script>
|
||||
|
||||
<script src="js/battle-dex-data.js"></script>
|
||||
<script src="data/pokedex.js"></script>
|
||||
<script src="data/moves.js"></script>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user