Support PM windows

These are now called `mini-window`s by the client code, and they're now
generic in the sense that any PS room can be a mini-window, not just
PMs and News.

This also adds BattleTextParser as a dependency of client-main,
removing some duplicate code in exchange for a hopefully-negligible
difference in load time.
This commit is contained in:
Guangcong Luo
2019-10-15 12:47:24 +10:30
parent 0d05feb41b
commit 5d6304eeb6
14 changed files with 189 additions and 119 deletions

View File

@@ -54,6 +54,7 @@
<script defer src="/js/client-core.js?"></script>
<script defer src="/js/battle-dex.js?"></script>
<script defer src="/js/battle-text-parser.js"></script>
<script defer src="/js/client-main.js?"></script>
<script defer src="/js/lib/sockjs-1.4.0-nwjsfix.min.js?"></script>
<script defer src="/js/client-connection.js?"></script>
@@ -68,14 +69,13 @@
<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/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="/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>

View File

@@ -1,3 +1,13 @@
/**
* Text parser
*
* No dependencies
* Optional dependency: BattleText
*
* @author Guangcong Luo <guangcongluo@gmail.com>
* @license MIT
*/
declare const BattleText: {[id: string]: {[templateName: string]: string}};
type Args = [string, ...string[]];
@@ -15,12 +25,14 @@ class BattleTextParser {
this.perspective = perspective;
}
static parseLine(line: string): {args: Args, kwArgs: KWArgs} {
static parseLine(line: string, noDefault: true): Args | null;
static parseLine(line: string): Args;
static parseLine(line: string, noDefault?: boolean): Args | null {
if (!line.startsWith('|')) {
return {args: ['', line], kwArgs: {}};
return ['', line];
}
if (line === '|') {
return {args: ['done'], kwArgs: {}};
return ['done'];
}
const index = line.indexOf('|', 1);
const cmd = line.slice(1, index);
@@ -28,23 +40,28 @@ class BattleTextParser {
case 'chatmsg': case 'chatmsg-raw': case 'raw': case 'error': case 'html':
case 'inactive': case 'inactiveoff': case 'warning':
case 'fieldhtml': case 'controlshtml': case 'bigerror':
case 'debug': case 'tier':
return {args: [cmd, line.slice(index + 1)], kwArgs: {}};
case 'debug': case 'tier': case 'challstr': case 'popup': case '':
return [cmd, line.slice(index + 1)];
case 'c': case 'chat': case 'uhtml': case 'uhtmlchange':
// three parts
const index2a = line.indexOf('|', index + 1);
return {args: [cmd, line.slice(index + 1, index2a), line.slice(index2a + 1)], kwArgs: {}};
case 'c:':
return [cmd, line.slice(index + 1, index2a), line.slice(index2a + 1)];
case 'c:': case 'pm':
// four parts
const index2b = line.indexOf('|', index + 1);
const index3b = line.indexOf('|', index2b + 1);
return {
args: [cmd, line.slice(index + 1, index2b), line.slice(index2b + 1, index3b), line.slice(index3b + 1)],
kwArgs: {},
};
return [cmd, line.slice(index + 1, index2b), line.slice(index2b + 1, index3b), line.slice(index3b + 1)];
}
let args: Args = line.slice(1).split('|') as any;
let kwArgs: KWArgs = {};
if (noDefault) return null;
return line.slice(1).split('|') as [string, ...string[]];
}
static parseBattleLine(line: string): {args: Args, kwArgs: KWArgs} {
let args = this.parseLine(line, true);
if (args) return {args, kwArgs: {}};
args = line.slice(1).split('|') as [string, ...string[]];
const kwArgs: KWArgs = {};
while (args.length > 1) {
const lastArg = args[args.length - 1];
if (lastArg.charAt(0) !== '[') break;
@@ -159,7 +176,7 @@ class BattleTextParser {
extractMessage(buf: string) {
let out = '';
for (const line of buf.split('\n')) {
const {args, kwArgs} = BattleTextParser.parseLine(line);
const {args, kwArgs} = BattleTextParser.parseBattleLine(line);
out += this.parseArgs(args, kwArgs) || '';
}
return out;

View File

@@ -3395,7 +3395,7 @@ class Battle {
return;
}
if (!str) return;
const {args, kwArgs} = BattleTextParser.parseLine(str);
const {args, kwArgs} = BattleTextParser.parseBattleLine(str);
if (this.scene.maybeCloseMessagebar(args, kwArgs)) {
this.activityStep--;
@@ -3408,7 +3408,7 @@ class Battle {
let nextKwargs: KWArgs = {};
const nextLine = this.activityQueue[this.activityStep + 1] || '';
if (nextLine.slice(0, 2) === '|-') {
({args: nextArgs, kwArgs: nextKwargs} = BattleTextParser.parseLine(nextLine));
({args: nextArgs, kwArgs: nextKwargs} = BattleTextParser.parseBattleLine(nextLine));
}
if (this.debug) {

View File

@@ -82,11 +82,10 @@ if (!window.console) {
*********************************************************************/
// PS's model classes are defined here
const PSURL = (() => {
let prefix = '';
if (document.location!.protocol !== 'http:') prefix = 'https:';
return prefix + '//play.pokemonshowdown.com/';
})();
const PSURL = (
(document.location!.protocol !== 'http:' ? 'https:' : '') +
'//play.pokemonshowdown.com/'
);
class PSSubscription {
observable: PSModel | PSStreamModel<any>;

View File

@@ -224,13 +224,16 @@ class PSTeams extends PSStreamModel<'team' | 'format'> {
class PSUser extends PSModel {
name = "Guest";
group = '';
userid = "guest" as ID;
named = false;
registered = false;
avatar = "1";
setName(name: string, named: boolean, avatar: string) {
setName(fullName: string, named: boolean, avatar: string) {
const loggingIn = (!this.named && named);
const {name, group} = BattleTextParser.parseNameParts(fullName);
this.name = name;
this.group = group;
this.userid = toID(name);
this.named = named;
this.avatar = avatar;
@@ -336,7 +339,7 @@ class PSServer {
* Rooms
*********************************************************************/
type PSRoomLocation = 'left' | 'right' | 'popup' | 'modal-popup' | 'semimodal-popup';
type PSRoomLocation = 'left' | 'right' | 'popup' | 'mini-window' | 'modal-popup' | 'semimodal-popup';
interface RoomOptions {
id: RoomID;
@@ -427,7 +430,7 @@ class PlaceholderRoom extends PSRoom {
* PS
*********************************************************************/
type RoomType = {Model: typeof PSRoom, Component: any, title?: string};
type RoomType = {Model?: typeof PSRoom, Component: any, title?: string};
/**
* This model updates:
@@ -464,6 +467,8 @@ const PS = new class extends PSModel {
leftRoomList: RoomID[] = [];
/** List of rooms on the right side of the top tabbar */
rightRoomList: RoomID[] = [];
/** List of mini-rooms in the Main Menu */
miniRoomList: RoomID[] = [];
/** Currently active popups, in stack order (bottom to top) */
popups: RoomID[] = [];
@@ -527,54 +532,32 @@ const PS = new class extends PSModel {
/** Tracks whether or not to display the "Use arrow keys" hint */
arrowKeysUsed = false;
newsHTML = document.querySelector('.news-embed .pm-log')?.innerHTML || '';
constructor() {
super();
this.addRoom({
id: '' as RoomID,
title: "Home",
type: 'mainmenu',
});
this.addRoom({
id: 'rooms' as RoomID,
title: "Rooms",
type: 'rooms',
});
if (this.newsHTML) {
this.addRoom({
id: 'news' as RoomID,
title: "News",
});
}
this.updateLayout();
window.addEventListener('resize', () => this.updateLayout());
}
lineParse(str: string): [string, ...string[]] {
if (!str.startsWith('|')) {
return ['', str];
}
const index = str.indexOf('|', 1);
const cmd = str.slice(1, index);
switch (cmd) {
case 'html':
case 'raw':
case 'challstr':
case 'popup':
case '':
return [cmd, str.slice(index + 1)];
case 'c':
case 'uhtml':
case 'uhtmlchange':
// three parts
const index2a = str.indexOf('|', index + 1);
return [cmd, str.slice(index + 1, index2a), str.slice(index2a + 1)];
case 'c:':
case 'pm':
// four parts
const index2b = str.indexOf('|', index + 1);
const index3b = str.indexOf('|', index2b + 1);
return [cmd, str.slice(index + 1, index2b), str.slice(index2b + 1, index3b), str.slice(index3b + 1)];
}
return str.slice(1).split('|') as [string, ...string[]];
}
// Panel layout
///////////////
/**
@@ -750,6 +733,7 @@ const PS = new class extends PSModel {
switch (hyphenIndex < 0 ? options.id : options.id.slice(0, hyphenIndex + 1)) {
case 'teambuilder': case 'ladder': case 'battles': case 'rooms':
case 'options': case 'volume': case 'teamdropdown': case 'formatdropdown':
case 'news':
options.type = options.id;
break;
case 'battle-': case 'user-': case 'team-':
@@ -758,6 +742,9 @@ const PS = new class extends PSModel {
case 'view-':
options.type = 'html';
break;
case '':
options.type = 'mainmenu';
break;
default:
options.type = 'chat';
break;
@@ -779,12 +766,16 @@ const PS = new class extends PSModel {
case 'formatdropdown':
options.location = 'semimodal-popup';
break;
case 'news':
options.location = 'mini-window';
break;
}
if (options.id.startsWith('pm-')) options.location = 'mini-window';
}
const roomType = this.roomTypes[options.type];
if (roomType?.title) options.title = roomType.title;
const Model = roomType ? roomType.Model : PlaceholderRoom;
const Model = roomType ? (roomType.Model || PSRoom) : PlaceholderRoom;
return new Model(options);
}
updateRoomTypes() {
@@ -797,7 +788,8 @@ const PS = new class extends PSModel {
const options: RoomOptions = room;
if (roomType.title) options.title = roomType.title;
const newRoom = new roomType.Model(options);
const Model = roomType.Model || PSRoom;
const newRoom = new Model(options);
this.rooms[roomid] = newRoom;
if (this.leftRoom === room) this.leftRoom = newRoom;
if (this.rightRoom === room) this.rightRoom = newRoom;
@@ -937,6 +929,9 @@ const PS = new class extends PSModel {
}
if (!noFocus || !this.rightRoom) this.rightRoom = room;
break;
case 'mini-window':
this.miniRoomList.push(room.id);
break;
case 'popup':
case 'semimodal-popup':
case 'modal-popup':
@@ -979,6 +974,13 @@ const PS = new class extends PSModel {
if (PS.room === room) PS.room = PS.activePanel;
}
if (room.location === 'mini-window') {
const miniRoomIndex = PS.miniRoomList.indexOf(room.id);
if (miniRoomIndex >= 0) {
PS.miniRoomList.splice(miniRoomIndex, 1);
}
}
if (this.popups.length && room.id === this.popups[this.popups.length - 1]) {
this.popups.pop();
PS.room = this.popups.length ? PS.rooms[this.popups[this.popups.length - 1]]! : PS.activePanel;

View File

@@ -360,6 +360,7 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
};
render() {
const room = this.props.room;
const isPM = !!room.pmTarget;
const challengeTo = room.challengingFormat ? <div class="challenge">
<TeamForm format={room.challengingFormat} onSubmit={null}>
@@ -381,11 +382,11 @@ class ChatPanel extends PSRoomPanel<ChatRoom> {
return <PSPanelWrapper room={room}>
<div class="tournament-wrapper hasuserlist"></div>
<ChatLog class="chat-log" room={this.props.room} onClick={this.focusIfNoSelection} left={146}>
<ChatLog class="chat-log" room={this.props.room} onClick={this.focusIfNoSelection} left={isPM ? 0 : 146}>
{challengeTo || challengeFrom && [challengeTo, challengeFrom]}
</ChatLog>
<ChatTextEntry room={this.props.room} onMessage={this.send} onKey={this.onKey} left={146} />
<ChatUserList room={this.props.room} />
<ChatTextEntry room={this.props.room} onMessage={this.send} onKey={this.onKey} left={isPM ? 0 : 146} />
<ChatUserList room={this.props.room} minimized={isPM} />
</PSPanelWrapper>;
}
}
@@ -455,7 +456,7 @@ class ChatLog extends preact.Component<{
}
this.subscription = this.props.room.subscribe(msg => {
if (!msg) return;
const tokens = PS.lineParse(msg);
const tokens = BattleTextParser.parseLine(msg);
switch (tokens[0]) {
case 'title':
this.props.room.title = tokens[1];

View File

@@ -7,6 +7,8 @@
* @license AGPLv3
*/
// Example room with panel
class ExampleRoom extends PSRoom {
readonly classType: string = 'example';
constructor(options: RoomOptions) {
@@ -27,3 +29,18 @@ PS.roomTypes['example'] = {
Model: ExampleRoom,
Component: ExamplePanel,
};
// Example panel with no room
class ExampleViewPanel extends PSRoomPanel {
render() {
const room = this.props.room;
return <PSPanelWrapper room={room}>
<div class="mainmessage"><p>Loading...</p></div>
</PSPanelWrapper>;
}
}
PS.roomTypes['exampleview'] = {
Component: ExampleViewPanel,
};

View File

@@ -23,7 +23,7 @@ class MainMenuRoom extends PSRoom {
pspl?: RoomInfo[],
} = {};
receive(line: string) {
const tokens = PS.lineParse(line);
const tokens = BattleTextParser.parseLine(line);
switch (tokens[0]) {
case 'challstr':
PSLoginServer.query({
@@ -242,13 +242,41 @@ class MainMenuRoom extends PSRoom {
}
}
class MainMenuPanel extends PSRoomPanel {
class NewsPanel extends PSRoomPanel {
render() {
return <PSPanelWrapper room={this.props.room} scrollable>
<div class="mini-window-body" dangerouslySetInnerHTML={{__html: PS.newsHTML}}></div>
</PSPanelWrapper>;
}
}
class MainMenuPanel extends PSRoomPanel<MainMenuRoom> {
focus() {
(this.base!.querySelector('button.big') as HTMLButtonElement).focus();
}
submit = (e: Event) => {
alert('todo: implement');
};
renderMiniRoom(room: PSRoom) {
const roomType = PS.roomTypes[room.type];
const Panel = roomType ? roomType.Component : PSRoomPanel;
return <Panel key={room.id} room={room} />;
}
renderMiniRooms() {
return PS.miniRoomList.map(roomid => {
const room = PS.rooms[roomid]!;
return <div class="pmbox">
<div class="mini-window">
<h3>
<button class="closebutton" name="closeRoom" value={roomid} aria-label="Close" tabIndex={-1}><i class="fa fa-times-circle"></i></button>
<button class="minimizebutton" tabIndex={-1}><i class="fa fa-minus-circle"></i></button>
{room.title}
</h3>
{this.renderMiniRoom(room)}
</div>
</div>;
});
}
render() {
const onlineButton = ' button' + (PS.isOffline ? ' disabled' : '');
const searchButton = (PS.down ? <div class="menugroup" style="background: rgba(10,10,10,.6)">
@@ -274,22 +302,7 @@ class MainMenuPanel extends PSRoomPanel {
<div class="mainmenuwrapper">
<div class="leftmenu">
<div class="activitymenu">
<div class="pmbox">
<div class="pm-window news-embed" data-newsid="<!-- newsid -->">
<h3>
<button class="closebutton" tabIndex={-1}><i class="fa fa-times-circle"></i></button>
<button class="minimizebutton" tabIndex={-1}><i class="fa fa-minus-circle"></i></button>
News
</h3>
<div class="pm-log" style="max-height:none">
<div class="newsentry">
<h4>Test client</h4>
<p>Welcome to the test client! You can test client changes here!</p>
<p>&mdash;<strong>Zarel</strong> <small class="date">on Sep 25, 2015</small></p>
</div>
</div>
</div>
</div>
{this.renderMiniRooms()}
</div>
<div class="mainmenu">
{searchButton}
@@ -437,6 +450,10 @@ class TeamForm extends preact.Component<{
}
}
PS.roomTypes['news'] = {
Component: NewsPanel,
};
PS.roomTypes['mainmenu'] = {
Model: MainMenuRoom,
Component: MainMenuPanel,

View File

@@ -190,7 +190,6 @@ class TeamPanel extends PSRoomPanel {
}
PS.roomTypes['team'] = {
Model: PSRoom,
Component: TeamPanel,
title: "Team",
};

View File

@@ -221,7 +221,6 @@ class TeambuilderPanel extends PSRoomPanel {
}
PS.roomTypes['teambuilder'] = {
Model: PSRoom,
Component: TeambuilderPanel,
title: "Teambuilder",
};

View File

@@ -624,11 +624,9 @@ class FormatDropdownPanel extends PSRoomPanel {
}
PS.roomTypes['teamdropdown'] = {
Model: PSRoom,
Component: TeamDropdownPanel,
};
PS.roomTypes['formatdropdown'] = {
Model: PSRoom,
Component: FormatDropdownPanel,
};

View File

@@ -142,6 +142,12 @@ function PSPanelWrapper(props: {
room: PSRoom, children: preact.ComponentChildren, scrollable?: boolean, width?: number,
}) {
const room = props.room;
if (room.location === 'mini-window') {
if (room.id === 'news') {
return <div>{props.children}</div>;
}
return <div id={`room-${room.id}`} class="mini-window-contents ps-room-light">{props.children}</div>;
}
if (room.location !== 'left' && room.location !== 'right') {
const style = PSMain.getPopupStyle(room, props.width);
return <div class="ps-popup" id={`room-${room.id}`} style={style}>

View File

@@ -531,10 +531,10 @@ select {
color: #BB2222;
}
.minimizebutton:hover,
.pm-window h3:hover .minimizebutton {
.mini-window h3:hover .minimizebutton {
color: #333333;
}
.pm-window h3 .closebutton:hover + .minimizebutton {
.mini-window h3 .closebutton:hover + .minimizebutton {
color: #999999 !important;
}
.closebutton:active,
@@ -624,11 +624,11 @@ select {
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
}
.ps-room.ps-room-light {
.ps-room.ps-room-light, .ps-room-light {
background: rgba(242,247,250,.85);
color: black;
}
.ps-room.ps-room-opaque {
.ps-room.ps-room-opaque, .ps-room-opaque {
background: #EEF2F5;
color: black;
}
@@ -824,13 +824,13 @@ p.or:after {
margin: 0 auto;
min-height: 2px;
}
.pm-window {
.mini-window {
margin: 0 -12px 12px -10px;
}
.tiny-layout .pm-window {
.tiny-layout .mini-window {
margin: 0 0 12px 0;
}
.pm-window h3 {
.mini-window h3 {
background: rgba(248,248,248,.8);
color: #777777;
margin: 0;
@@ -841,33 +841,41 @@ p.or:after {
border-top-right-radius: 5px;
cursor: pointer;
}
.pm-window h3:hover {
.mini-window h3:hover {
color: #333333;
}
.pm-window h3.pm-minimized {
.mini-window h3.pm-minimized {
border-radius: 5px;
}
.pm-window h3.pm-notifying {
.mini-window h3.pm-notifying {
border-color: #AA8866;
background: #E3C3A3;
}
.pm-window h3.pm-notifying:hover {
.mini-window h3.pm-notifying:hover {
border-color: #604020;
background: #CFAF8F;
}
.pm-window h3 .closebutton,
.pm-window h3 .minimizebutton {
.mini-window h3 .closebutton,
.mini-window h3 .minimizebutton {
float: right;
margin: -3px -3px;
width: 22px;
height: 22px;
}
.pm-window h3 small {
.mini-window h3 small {
color: #888888;
font-weight: normal;
vertical-align: 1px;
}
.pm-log {
.mini-window .mini-window-contents {
position: relative;
height: 300px;
border: 1px solid #AAAAAA;
border-top: 0;
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.mini-window-body, .pm-log {
min-height: 100px;
max-height: 300px;
background: rgba(242,247,250,.925);
@@ -875,12 +883,18 @@ p.or:after {
word-wrap: break-word;
border: 1px solid #AAAAAA;
border-top: 0;
border-bottom: 0;
overflow: auto;
-webkit-overflow-scrolling: touch;
overflow-scrolling: touch;
}
.mini-window-body {
border-bottom-left-radius: 5px;
border-bottom-right-radius: 5px;
}
.pm-log {
border-bottom: 0;
}
.pm-log-add {
color: black;
border: 1px solid #AAAAAA;
@@ -888,17 +902,17 @@ p.or:after {
border-bottom-right-radius: 5px;
background: rgba(242,247,250,.85);
}
.pm-window.collapsed .pm-log,
.pm-window.collapsed .pm-log-add {
.mini-window.collapsed .pm-log,
.mini-window.collapsed .pm-log-add {
display: none;
}
.pm-window.focused h3,
.pm-window.focused .pm-log,
.pm-window.focused .pm-log-add {
.mini-window.focused h3,
.mini-window.focused .pm-log,
.mini-window.focused .pm-log-add {
border-color: #555555;
}
.pm-window.focused h3,
.pm-window.focused h3:hover {
.mini-window.focused h3,
.mini-window.focused h3:hover {
background: #f8f8f8;
color: #222222;
}
@@ -916,6 +930,7 @@ p.or:after {
font-size: 9pt;
padding: 1px 10px;
border-bottom: 1px solid #AAAAAA;
margin-bottom: -1px;
}
.unread {
background: rgba(255,240,130,0.33);
@@ -1220,7 +1235,7 @@ p.or:after {
}
.chat-log .inner,
.pm-log .inner {
padding: 4px 8px 6px 8px;
padding: 24px 8px 6px 8px;
}
a.ilink {
text-decoration: none;
@@ -3286,9 +3301,9 @@ a.ilink.yours {
.dark .menugroup {
background: rgba(0,0,0,.2);
}
.dark .pm-window.focused h3,
.dark .pm-window.focused .pm-log,
.dark .pm-window.focused .pm-log-add {
.dark .mini-window.focused h3,
.dark .mini-window.focused .pm-log,
.dark .mini-window.focused .pm-log-add {
border-color: #CCC;
}

View File

@@ -55,6 +55,7 @@
<script src="js/client-core.js"></script>
<script src="js/battle-dex.js"></script>
<script src="js/battle-text-parser.js"></script>
<script src="js/client-main.js"></script>
<script src="js/lib/sockjs-1.4.0-nwjsfix.min.js"></script>
<script src="js/client-connection.js"></script>
@@ -72,7 +73,6 @@
<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>