mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-04-24 23:30:37 -05:00
Preact: Refactor client commands
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run
Some checks are pending
Node.js CI / build (22.x) (push) Waiting to run
At this point we've probably outgrown the "huge switch" pattern. So instead, here's a client command syntax a lot like server's. I've also added an `add` command that works more like server's and 2013 client's, as the default way to respond to client commands.
This commit is contained in:
parent
8f41ceb4da
commit
0740eb5148
|
|
@ -316,7 +316,7 @@ export const defaultRulesES3 = {
|
|||
ignoreRestSiblings: true,
|
||||
}],
|
||||
"no-restricted-syntax": ["error",
|
||||
{ selector: "TaggedTemplateExpression", message: "Hard to compile down to ES3" },
|
||||
{ selector: "TaggedTemplateExpression", message: "Not supported by ES3" },
|
||||
{ selector: "CallExpression[callee.name='Symbol']", message: "Annoying to serialize, just use a string" },
|
||||
],
|
||||
|
||||
|
|
@ -344,7 +344,7 @@ export const defaultRulesES3TSChecked = {
|
|||
...defaultRulesTSChecked,
|
||||
"radix": "off",
|
||||
"no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet", "WeakMap", "Set", "Map"],
|
||||
"no-restricted-syntax": ["error", "TaggedTemplateExpression", "YieldExpression", "AwaitExpression", "BigIntLiteral"],
|
||||
"no-restricted-syntax": ["error", "YieldExpression", "AwaitExpression", "BigIntLiteral"],
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1072,6 +1072,18 @@ export class BattleLog {
|
|||
if (jsEscapeToo) str = str.replace(/\\/g, '\\\\').replace(/'/g, '\\\'');
|
||||
return str;
|
||||
}
|
||||
/**
|
||||
* Template string tag function for escaping HTML
|
||||
*/
|
||||
static html(strings: TemplateStringsArray | string[], ...args: any) {
|
||||
let buf = strings[0];
|
||||
let i = 0;
|
||||
while (i < args.length) {
|
||||
buf += this.escapeHTML(args[i]);
|
||||
buf += strings[++i];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
static unescapeHTML(str: string) {
|
||||
str = (str ? '' + str : '');
|
||||
|
|
@ -1165,7 +1177,7 @@ export class BattleLog {
|
|||
name = name.substr(1);
|
||||
}
|
||||
const colorStyle = ` style="color:${BattleLog.usernameColor(toID(name))}"`;
|
||||
const clickableName = `<small>${BattleLog.escapeHTML(group)}</small><span class="username">${BattleLog.escapeHTML(name)}</span>`;
|
||||
const clickableName = `<small class="groupsymbol">${BattleLog.escapeHTML(group)}</small><span class="username">${BattleLog.escapeHTML(name)}</span>`;
|
||||
let hlClass = isHighlighted ? ' highlighted' : '';
|
||||
let isMine = (window.app?.user?.get('name') === name) || (window.PS?.user.name === name);
|
||||
let mineClass = isMine ? ' mine' : '';
|
||||
|
|
|
|||
|
|
@ -626,6 +626,14 @@ interface PSNotificationState {
|
|||
noAutoDismiss: boolean;
|
||||
}
|
||||
|
||||
type ClientCommands<RoomT extends PSRoom> = {
|
||||
[command: Lowercase<string>]: (this: RoomT, target: string, cmd: string) => string | boolean | null | void,
|
||||
};
|
||||
/** The command signature is a lie but TypeScript and string validation amirite? */
|
||||
type ParsedClientCommands = {
|
||||
[command: `parsed${string}`]: (this: PSRoom, target: string, cmd: string) => string | boolean | null | void,
|
||||
};
|
||||
|
||||
/**
|
||||
* As a PSStreamModel, PSRoom can emit `Args` to mean "we received a message",
|
||||
* and `null` to mean "tell Preact to re-render this room"
|
||||
|
|
@ -744,38 +752,83 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
|
|||
}
|
||||
}
|
||||
/**
|
||||
* Handles outgoing messages, like `/logout`. Return `true` to prevent
|
||||
* the line from being sent to servers.
|
||||
* Used only by commands; messages from the server go directly from
|
||||
* `PS.receive` to `room.receiveLine`
|
||||
*/
|
||||
handleSend(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 'logout':
|
||||
add(line: string) {
|
||||
if (this.type !== 'chat' && this.type !== 'battle') {
|
||||
PS.mainmenu.handlePM(PS.user.userid, PS.user.userid);
|
||||
PS.rooms['dm-' as RoomID]?.receiveLine(BattleTextParser.parseLine(line));
|
||||
} else {
|
||||
this.receiveLine(BattleTextParser.parseLine(line));
|
||||
}
|
||||
}
|
||||
parseClientCommands(commands: ClientCommands<this>) {
|
||||
const parsedCommands: ParsedClientCommands = {};
|
||||
for (const cmd in commands) {
|
||||
const names = cmd.split(',').map(name => name.trim());
|
||||
for (const name of names) {
|
||||
if (name.includes(' ')) throw new Error(`Client command names cannot contain spaces: ${name}`);
|
||||
// good luck convincing TypeScript that these types are compatible
|
||||
parsedCommands[name as 'parsed'] = commands[cmd as 'cmd'] as any;
|
||||
}
|
||||
}
|
||||
return parsedCommands;
|
||||
}
|
||||
globalClientCommands = this.parseClientCommands({
|
||||
'j,join'(target) {
|
||||
const roomid = /[^a-z0-9-]/.test(target) ? toID(target) as any as RoomID : target as RoomID;
|
||||
PS.join(roomid);
|
||||
},
|
||||
'part,leave,close'(target) {
|
||||
const roomid = /[^a-z0-9-]/.test(target) ? toID(target) as any as RoomID : target as RoomID;
|
||||
PS.leave(roomid || this.id);
|
||||
},
|
||||
'logout'() {
|
||||
PS.user.logOut();
|
||||
return true;
|
||||
case 'cancelsearch':
|
||||
},
|
||||
'cancelsearch'() {
|
||||
PS.mainmenu.cancelSearch();
|
||||
return true;
|
||||
case 'nick':
|
||||
},
|
||||
'nick'(target) {
|
||||
if (target) {
|
||||
PS.user.changeName(target);
|
||||
} else {
|
||||
PS.join('login' as RoomID);
|
||||
}
|
||||
return true;
|
||||
case 'open':
|
||||
case 'user': {
|
||||
},
|
||||
'open,user'(target) {
|
||||
let roomid = `user-${toID(target)}` as RoomID;
|
||||
PS.join(roomid, {
|
||||
args: { username: target },
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'showjoins': {
|
||||
},
|
||||
'ignore'(target) {
|
||||
const ignore = PS.prefs.ignore || {};
|
||||
if (!target) return true;
|
||||
if (toID(target) === PS.user.userid) {
|
||||
this.add(`||You are not able to ignore yourself.`);
|
||||
} else if (ignore[toID(target)]) {
|
||||
this.add(`||User '${target}' is already on your ignore list. ` +
|
||||
`(Moderator messages will not be ignored.)`);
|
||||
} else {
|
||||
ignore[toID(target)] = 1;
|
||||
this.add(`||User '${target}' ignored. (Moderator messages will not be ignored.)`);
|
||||
PS.prefs.set("ignore", ignore);
|
||||
}
|
||||
},
|
||||
'unignore'(target) {
|
||||
const ignore = PS.prefs.ignore || {};
|
||||
if (!target) return true;
|
||||
if (!ignore[toID(target)]) {
|
||||
this.add(`||User '${target}' isn't on your ignore list.`);
|
||||
} else {
|
||||
ignore[toID(target)] = 0;
|
||||
this.add(`||User '${target}' no longer ignored.`);
|
||||
PS.prefs.set("ignore", ignore);
|
||||
}
|
||||
},
|
||||
'showjoins'(target) {
|
||||
let showjoins = PS.prefs.showjoins || {};
|
||||
let serverShowjoins = showjoins[PS.server.id] || {};
|
||||
if (target) {
|
||||
|
|
@ -785,17 +838,15 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
|
|||
} else {
|
||||
serverShowjoins[room] = 1;
|
||||
}
|
||||
this.receiveLine([`c`, "", `Join/leave messages on room ${room}: ALWAYS ON`]);
|
||||
this.add(`||Join/leave messages in room ${room}: ALWAYS ON`);
|
||||
} else {
|
||||
serverShowjoins = { global: 1 };
|
||||
this.receiveLine([`c`, "", `Join/leave messages: ALWAYS ON`]);
|
||||
this.add(`||Join/leave messages: ALWAYS ON`);
|
||||
}
|
||||
showjoins[PS.server.id] = serverShowjoins;
|
||||
PS.prefs.set("showjoins", showjoins);
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'hidejoins': {
|
||||
},
|
||||
'hidejoins'(target) {
|
||||
let showjoins = PS.prefs.showjoins || {};
|
||||
let serverShowjoins = showjoins[PS.server.id] || {};
|
||||
if (target) {
|
||||
|
|
@ -805,28 +856,41 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
|
|||
} else {
|
||||
serverShowjoins[room] = 0;
|
||||
}
|
||||
this.receiveLine([`c`, "", `Join/leave messages on room ${room}: OFF`]);
|
||||
this.add(`||Join/leave messages on room ${room}: OFF`);
|
||||
} else {
|
||||
serverShowjoins = { global: 0 };
|
||||
this.receiveLine([`c`, "", `Join/leave messages: OFF`]);
|
||||
|
||||
this.add(`||Join/leave messages: OFF`);
|
||||
}
|
||||
showjoins[PS.server.id] = serverShowjoins;
|
||||
PS.prefs.set('showjoins', showjoins);
|
||||
return true;
|
||||
},
|
||||
});
|
||||
clientCommands: ParsedClientCommands | null = null;
|
||||
/**
|
||||
* Handles outgoing messages, like `/logout`. Return `true` to prevent
|
||||
* the line from being sent to servers.
|
||||
*/
|
||||
handleSend(line: string) {
|
||||
if (!line.startsWith('/') || line.startsWith('//')) return line;
|
||||
const spaceIndex = line.indexOf(' ');
|
||||
const cmd = (spaceIndex >= 0 ? line.slice(1, spaceIndex) : line.slice(1)) as 'parsed';
|
||||
const target = spaceIndex >= 0 ? line.slice(spaceIndex + 1) : '';
|
||||
|
||||
}
|
||||
const cmdHandler = this.globalClientCommands[cmd] || this.clientCommands?.[cmd];
|
||||
if (!cmdHandler) return line;
|
||||
|
||||
}
|
||||
return false;
|
||||
const cmdResult = cmdHandler.call(this, target, cmd);
|
||||
if (cmdResult === true) return line;
|
||||
return cmdResult || null;
|
||||
}
|
||||
send(msg: string) {
|
||||
send(msg: string | null) {
|
||||
if (!msg) return;
|
||||
msg = this.handleSend(msg);
|
||||
if (!msg) return;
|
||||
if (this.handleSend(msg)) return;
|
||||
this.sendDirect(msg);
|
||||
}
|
||||
sendDirect(msg: string) {
|
||||
PS.send(this.id + '|' + msg);
|
||||
PS.send(`${this.id}|${msg}`);
|
||||
}
|
||||
destroy() {
|
||||
if (this.connected) {
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ class BattlesPanel extends PSRoomPanel<BattlesRoom> {
|
|||
}
|
||||
}
|
||||
|
||||
class BattleRoom extends ChatRoom {
|
||||
export class BattleRoom extends ChatRoom {
|
||||
override readonly classType = 'battle';
|
||||
declare pmTarget: null;
|
||||
declare challengeMenuOpen: false;
|
||||
|
|
@ -127,73 +127,6 @@ class BattleRoom extends ChatRoom {
|
|||
side: BattleRequestSideInfo | null = null;
|
||||
request: BattleRequest | null = null;
|
||||
choices: BattleChoiceBuilder | null = null;
|
||||
|
||||
/**
|
||||
* @return true to prevent line from being sent to server
|
||||
*/
|
||||
override handleSend(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': {
|
||||
if (this.battle.atQueueEnd) {
|
||||
this.battle.reset();
|
||||
}
|
||||
this.battle.play();
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'pause': {
|
||||
this.battle.pause();
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'ffto': case 'fastfowardto': {
|
||||
let turnNum = Number(target);
|
||||
if (target.startsWith('+') || turnNum < 0) {
|
||||
turnNum += this.battle.turn;
|
||||
if (turnNum < 0) turnNum = 0;
|
||||
} else if (target === 'end') {
|
||||
turnNum = Infinity;
|
||||
}
|
||||
if (isNaN(turnNum)) {
|
||||
this.receiveLine([`error`, `/ffto - Invalid turn number: ${target}`]);
|
||||
return true;
|
||||
}
|
||||
this.battle.seekTurn(turnNum);
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'switchsides': {
|
||||
this.battle.switchViewpoint();
|
||||
return true;
|
||||
} case 'cancel': case 'undo': {
|
||||
if (!this.choices || !this.request) {
|
||||
this.receiveLine([`error`, `/choose - You are not a player in this battle`]);
|
||||
return true;
|
||||
}
|
||||
if (this.choices.isDone() || this.choices.isEmpty()) {
|
||||
this.sendDirect('/undo');
|
||||
}
|
||||
this.choices = new BattleChoiceBuilder(this.request);
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'move': case 'switch': case 'team': case 'pass': case 'shift': case 'choose': {
|
||||
if (!this.choices) {
|
||||
this.receiveLine([`error`, `/choose - You are not a player in this battle`]);
|
||||
return true;
|
||||
}
|
||||
const possibleError = this.choices.addChoice(line.slice(cmd === 'choose' ? 8 : 1));
|
||||
if (possibleError) {
|
||||
this.receiveLine([`error`, possibleError]);
|
||||
return true;
|
||||
}
|
||||
if (this.choices.isDone()) this.sendDirect(`/choose ${this.choices.toString()}`);
|
||||
this.update(null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return super.handleSend(line);
|
||||
}
|
||||
}
|
||||
|
||||
class BattleDiv extends preact.Component<{ room: BattleRoom }> {
|
||||
|
|
@ -799,18 +732,20 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
|
|||
<i class="fa fa-step-backward"></i><br />Prev turn
|
||||
</button>}
|
||||
</p>
|
||||
{room.side ?
|
||||
{room.side ? (
|
||||
<p>
|
||||
<button class="button" name="closeAndMainMenu" onClick={() => this.close()}>
|
||||
<button class="button" data-cmd="/close">
|
||||
<strong>Main menu</strong><br /><small>(closes this battle)</small>
|
||||
</button> {}
|
||||
<button
|
||||
class="button" name="cmd" value={`/closeandchallenge ${room.battle.farSide.id},${room.battle.tier}`}
|
||||
>
|
||||
<button class="button" data-cmd={`/closeandchallenge ${room.battle.farSide.id},${room.battle.tier}`}>
|
||||
<strong>Rematch</strong><br /><small>(closes this battle)</small>
|
||||
</button>
|
||||
</p> :
|
||||
<p><button class="button" name="cmd" value="/switchsides"><i class="fa fa-random"></i> Switch viewpoint</button></p>}
|
||||
</p>
|
||||
) : (
|
||||
<p>
|
||||
<button class="button" data-cmd="/switchsides"><i class="fa fa-random"></i> Switch viewpoint</button>
|
||||
</p>
|
||||
)}
|
||||
</div>;
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@ import { MiniEdit } from "./miniedit";
|
|||
import { PSUtils, toID, type ID } from "./battle-dex";
|
||||
import type { Args } from "./battle-text-parser";
|
||||
import { PSLoginServer } from "./client-connection";
|
||||
import type { BattleRoom } from "./panel-battle";
|
||||
import { BattleChoiceBuilder } from "./battle-choices";
|
||||
|
||||
declare const formatText: any; // from js/server/chat-formatter.js
|
||||
|
||||
|
|
@ -68,21 +70,20 @@ export class ChatRoom extends PSRoom {
|
|||
this.setUsers(count, usernames);
|
||||
return;
|
||||
|
||||
case 'join': case 'j': case 'J': {
|
||||
case 'join': case 'j': case 'J':
|
||||
this.addUser(args[1]);
|
||||
this.handleJoinLeave("join", args[1], args[0] === "J");
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'leave': case 'l': case 'L': {
|
||||
case 'leave': case 'l': case 'L':
|
||||
this.removeUser(args[1]);
|
||||
this.handleJoinLeave("leave", args[1], args[0] === "L");
|
||||
return true;
|
||||
}
|
||||
|
||||
case 'name': case 'n': case 'N':
|
||||
this.renameUser(args[1], args[2]);
|
||||
break;
|
||||
|
||||
case 'c':
|
||||
if (`${args[2]} `.startsWith('/challenge ')) {
|
||||
this.updateChallenge(args[1], args[2].slice(11));
|
||||
|
|
@ -97,9 +98,10 @@ export class ChatRoom extends PSRoom {
|
|||
super.receiveLine(args);
|
||||
}
|
||||
updateTarget(name?: string | null) {
|
||||
const selfWithGroup = `${PS.user.group || ' '}${PS.user.name}`;
|
||||
if (this.id === 'dm-') {
|
||||
this.pmTarget = PS.user.name;
|
||||
this.setUsers(1, [` ${PS.user.name}`]);
|
||||
this.pmTarget = selfWithGroup;
|
||||
this.setUsers(1, [selfWithGroup]);
|
||||
this.title = `Console`;
|
||||
} else if (this.id.startsWith('dm-')) {
|
||||
const id = this.id.slice(3);
|
||||
|
|
@ -111,79 +113,33 @@ export class ChatRoom extends PSRoom {
|
|||
if (!PS.user.userid) {
|
||||
this.setUsers(1, [nameWithGroup]);
|
||||
} else {
|
||||
this.setUsers(2, [nameWithGroup, ` ${PS.user.name}`]);
|
||||
this.setUsers(2, [nameWithGroup, selfWithGroup]);
|
||||
}
|
||||
this.title = `[DM] ${nameWithGroup.trim()}`;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* @return true to prevent line from being sent to server
|
||||
*/
|
||||
override handleSend(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 'j': case 'join': {
|
||||
const roomid = /[^a-z0-9-]/.test(target) ? toID(target) as any as RoomID : target as RoomID;
|
||||
PS.join(roomid);
|
||||
return true;
|
||||
} case 'part': case 'leave': {
|
||||
const roomid = /[^a-z0-9-]/.test(target) ? toID(target) as any as RoomID : target as RoomID;
|
||||
PS.leave(roomid || this.id);
|
||||
return true;
|
||||
} case 'chall': case 'challenge': case 'closeandchallenge': {
|
||||
override clientCommands = this.parseClientCommands({
|
||||
'chall,challenge,closeandchallenge'(target, cmd) {
|
||||
if (target) {
|
||||
const [targetUser, format] = target.split(',');
|
||||
PS.join(`challenge-${toID(targetUser)}` as RoomID);
|
||||
if (cmd === 'closeandchallenge') PS.leave(this.id);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
this.openChallenge();
|
||||
return true;
|
||||
} case 'cchall': case 'cancelchallenge': {
|
||||
},
|
||||
'cchall,cancelchallenge'(target) {
|
||||
this.cancelChallenge();
|
||||
return true;
|
||||
} case 'reject': {
|
||||
},
|
||||
'reject'(target) {
|
||||
this.challenged = null;
|
||||
this.update(null);
|
||||
return false;
|
||||
} case 'ignore': {
|
||||
let ignore = PS.prefs.ignore || {};
|
||||
if (!target) return true;
|
||||
if (toID(target) === PS.user.userid) {
|
||||
this.receiveLine(['', `You are not able to ignore yourself.`]);
|
||||
} else if (ignore[toID(target)]) {
|
||||
this.receiveLine(['', `User '${target}' is already on your ignore list. ` +
|
||||
`(Moderator messages will not be ignored.)`]);
|
||||
} else {
|
||||
ignore[toID(target)] = 1;
|
||||
this.receiveLine(['', `User '${target}' ignored. (Moderator messages will not be ignored.)`]);
|
||||
PS.prefs.set("ignore", ignore);
|
||||
}
|
||||
return true;
|
||||
} case 'unignore': {
|
||||
let ignore = PS.prefs.ignore || {};
|
||||
if (!target) return true;
|
||||
if (!ignore[toID(target)]) {
|
||||
this.receiveLine(['', `User '${target}' isn't on your ignore list.`]);
|
||||
} else {
|
||||
ignore[toID(target)] = 0;
|
||||
this.receiveLine(['', `User '${target}' no longer ignored.`]);
|
||||
PS.prefs.set("ignore", ignore);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
case 'clear': {
|
||||
},
|
||||
'clear'() {
|
||||
this.log?.reset();
|
||||
this.update(null);
|
||||
return true;
|
||||
}
|
||||
case 'rank':
|
||||
case 'ranking':
|
||||
case 'rating':
|
||||
case 'ladder': {
|
||||
},
|
||||
'rank,ranking,rating,ladder'(target) {
|
||||
let arg = target;
|
||||
if (!arg) {
|
||||
arg = PS.user.userid;
|
||||
|
|
@ -192,10 +148,10 @@ export class ChatRoom extends PSRoom {
|
|||
arg += ", " + this.id.split('-')[1];
|
||||
}
|
||||
|
||||
let targets = arg.split(',');
|
||||
const targets = arg.split(',');
|
||||
let formatTargeting = false;
|
||||
let formats: { [key: string]: number } = {};
|
||||
let gens: { [key: string]: number } = {};
|
||||
const formats: { [key: string]: number } = {};
|
||||
const gens: { [key: string]: number } = {};
|
||||
for (let i = 1, len = targets.length; i < len; i++) {
|
||||
targets[i] = $.trim(targets[i]);
|
||||
if (targets[i].length === 4 && targets[i].substr(0, 3) === 'gen') {
|
||||
|
|
@ -209,24 +165,24 @@ export class ChatRoom extends PSRoom {
|
|||
PSLoginServer.query("ladderget", {
|
||||
user: targets[0],
|
||||
}).then(data => {
|
||||
if (!data || !Array.isArray(data)) return this.receiveLine(['raw', 'Error: corrupted ranking data']);
|
||||
let buffer = '<div class="ladder"><table><tr><td colspan="9">User: <strong>' + toID(targets[0]) + '</strong></td></tr>';
|
||||
if (!data || !Array.isArray(data)) return this.add(`|error|Error: corrupted ranking data`);
|
||||
let buffer = `<div class="ladder"><table><tr><td colspan="9">User: <strong>${toID(targets[0])}</strong></td></tr>`;
|
||||
if (!data.length) {
|
||||
buffer += '<tr><td colspan="9"><em>This user has not played any ladder games yet.</em></td></tr>';
|
||||
buffer += '</table></div>';
|
||||
return this.receiveLine(['raw', buffer]);
|
||||
return this.add(`|html|${buffer}`);
|
||||
}
|
||||
buffer += '<tr><th>Format</th><th><abbr title="Elo rating">Elo</abbr></th><th><abbr title="user\'s percentage chance of winning a random battle (aka GLIXARE)">GXE</abbr></th><th><abbr title="Glicko-1 rating: rating±deviation">Glicko-1</abbr></th><th>COIL</th><th>W</th><th>L</th><th>Total</th>';
|
||||
buffer += '<tr><th>Format</th><th><abbr title="Elo rating">Elo</abbr></th><th><abbr title="user\'s percentage chance of winning a random battle (aka GLIXARE)">GXE</abbr></th><th><abbr title="Glicko-1 rating: rating ± deviation">Glicko-1</abbr></th><th>COIL</th><th>W</th><th>L</th><th>Total</th>';
|
||||
let suspect = false;
|
||||
for (let item of data) {
|
||||
for (const item of data) {
|
||||
if ('suspect' in item) suspect = true;
|
||||
}
|
||||
if (suspect) buffer += '<th>Suspect reqs possible?</th>';
|
||||
buffer += '</tr>';
|
||||
let hiddenFormats = [];
|
||||
for (let row of data) {
|
||||
if (!row) return this.receiveLine(['raw', 'Error: corrupted ranking data']);
|
||||
let formatId = toID(row.formatid);
|
||||
const hiddenFormats = [];
|
||||
for (const row of data) {
|
||||
if (!row) return this.add(`|error|Error: corrupted ranking data`);
|
||||
const formatId = toID(row.formatid);
|
||||
if (!formatTargeting ||
|
||||
formats[formatId] ||
|
||||
gens[formatId.slice(0, 4)] ||
|
||||
|
|
@ -238,37 +194,36 @@ export class ChatRoom extends PSRoom {
|
|||
}
|
||||
|
||||
// Validate all the numerical data
|
||||
let values = [row.elo, row.rpr, row.rprd, row.gxe, row.w, row.l, row.t];
|
||||
for (let value of values) {
|
||||
if (typeof value !== 'number' && typeof value !== 'string' ||
|
||||
isNaN(value as number)) return this.receiveLine(['raw', 'Error: corrupted ranking data']);
|
||||
for (const value of [row.elo, row.rpr, row.rprd, row.gxe, row.w, row.l, row.t]) {
|
||||
if (typeof value !== 'number' && typeof value !== 'string') {
|
||||
return this.add(`|error|Error: corrupted ranking data`);
|
||||
}
|
||||
}
|
||||
|
||||
buffer += `<td> ${BattleLog.escapeFormat(formatId)} </td><td><strong> ${Math.round(row.elo)} </strong></td>'`;
|
||||
buffer += `<td> ${BattleLog.escapeFormat(formatId)} </td><td><strong>${Math.round(row.elo)}</strong></td>`;
|
||||
if (row.rprd > 100) {
|
||||
// High rating deviation. Provisional rating.
|
||||
buffer += `<td>–</td>`;
|
||||
buffer += `<td><span><em> ${Math.round(row.rpr)} <small> ± ${Math.round(row.rprd)} </small></em> <small>(provisional)</small></span></td>`;
|
||||
buffer += `<td><span style="color:#888"><em>${Math.round(row.rpr)} <small> ± ${Math.round(row.rprd)} </small></em> <small>(provisional)</small></span></td>`;
|
||||
} else {
|
||||
let gxe = Math.round(row.gxe * 10);
|
||||
buffer += `<td> ${Math.floor(gxe / 10)} <small> ${(gxe % 10)}%</small></td>`;
|
||||
buffer += `<td><em> ${Math.round(row.rpr)} <small> ± ${Math.round(row.rprd)}</small></em></td>`;
|
||||
buffer += `<td>${Math.trunc(row.gxe)}<small>.${row.gxe.toFixed(1).slice(-1)}%</small></td>`;
|
||||
buffer += `<td><em>${Math.round(row.rpr)} <small> ± ${Math.round(row.rprd)}</small></em></td>`;
|
||||
}
|
||||
let N = parseInt(row.w, 10) + parseInt(row.l, 10) + parseInt(row.t, 10);
|
||||
let COIL_B = undefined;
|
||||
const N = parseInt(row.w, 10) + parseInt(row.l, 10) + parseInt(row.t, 10);
|
||||
const COIL_B = undefined;
|
||||
|
||||
// Uncomment this after LadderRoom logic is implemented
|
||||
// COIL_B = LadderRoom?.COIL_B[formatId];
|
||||
|
||||
if (COIL_B) {
|
||||
buffer += `<td> ${Math.round(40.0 * parseFloat(row.gxe) * 2.0 ** (-COIL_B / N))} </td>`;
|
||||
buffer += `<td>${Math.round(40.0 * parseFloat(row.gxe) * 2.0 ** (-COIL_B / N))}</td>`;
|
||||
} else {
|
||||
buffer += '<td>--</td>';
|
||||
buffer += '<td>—</td>';
|
||||
}
|
||||
buffer += `<td> ${row.w} </td><td> ${row.l} </td><td> ${N} </td>`;
|
||||
if (suspect) {
|
||||
if (typeof row.suspect === 'undefined') {
|
||||
buffer += '<td>--</td>';
|
||||
buffer += '<td>—</td>';
|
||||
} else {
|
||||
buffer += '<td>';
|
||||
buffer += (row.suspect ? "Yes" : "No");
|
||||
|
|
@ -279,27 +234,94 @@ export class ChatRoom extends PSRoom {
|
|||
}
|
||||
if (hiddenFormats.length) {
|
||||
if (hiddenFormats.length === data.length) {
|
||||
buffer += '<tr class="no-matches"><td colspan="8"><em>This user has not played any ladder games that match "' + BattleLog.escapeHTML(Object.keys(gens).concat(Object.keys(formats)).join(', ')) + '".</em></td></tr>';
|
||||
const formatsText = Object.keys(gens).concat(Object.keys(formats)).join(', ');
|
||||
buffer += `<tr class="no-matches"><td colspan="8">` +
|
||||
BattleLog.html`<em>This user has not played any ladder games that match ${formatsText}.</em></td></tr>`;
|
||||
}
|
||||
buffer += `<tr><td colspan="8"><button name="showOtherFormats"> ${hiddenFormats.slice(0, 3).join(', ') + (hiddenFormats.length > 3 ? ' and ' + String(hiddenFormats.length - 3) + ' other formats' : '')} not shown</button></td></tr>'`;
|
||||
const otherFormats = hiddenFormats.slice(0, 3).join(', ') +
|
||||
(hiddenFormats.length > 3 ? ` and ${hiddenFormats.length - 3} other formats` : '');
|
||||
buffer += `<tr><td colspan="8"><button name="showOtherFormats">` +
|
||||
BattleLog.html`${otherFormats} not shown</button></td></tr>`;
|
||||
}
|
||||
let userid = toID(targets[0]);
|
||||
let registered = PS.user.registered;
|
||||
if (registered && PS.user.userid === userid) {
|
||||
buffer += '<tr><td colspan="8" style="text-align:right"><a href="//' + PS.routes.users + '/' + userid + '">Reset W/L</a></tr></td>';
|
||||
buffer += `<tr><td colspan="8" style="text-align:right"><a href="//${PS.routes.users}/${userid}">Reset W/L</a></tr></td>`;
|
||||
}
|
||||
buffer += '</table></div>';
|
||||
this.receiveLine(['raw', buffer]);
|
||||
this.add(`|html|${buffer}`);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
}
|
||||
return super.handleSend(line);
|
||||
}
|
||||
// battle-specific commands
|
||||
// ------------------------
|
||||
'play'() {
|
||||
if (!this.battle) return this.add('|error|You are not in a battle');
|
||||
if (this.battle.atQueueEnd) {
|
||||
this.battle.reset();
|
||||
}
|
||||
this.battle.play();
|
||||
this.update(null);
|
||||
},
|
||||
'pause'() {
|
||||
if (!this.battle) return this.add('|error|You are not in a battle');
|
||||
this.battle.pause();
|
||||
this.update(null);
|
||||
},
|
||||
'ffto,fastfowardto'(target) {
|
||||
if (!this.battle) return this.add('|error|You are not in a battle');
|
||||
let turnNum = Number(target);
|
||||
if (target.startsWith('+') || turnNum < 0) {
|
||||
turnNum += this.battle.turn;
|
||||
if (turnNum < 0) turnNum = 0;
|
||||
} else if (target === 'end') {
|
||||
turnNum = Infinity;
|
||||
}
|
||||
if (isNaN(turnNum)) {
|
||||
this.receiveLine([`error`, `/ffto - Invalid turn number: ${target}`]);
|
||||
return;
|
||||
}
|
||||
this.battle.seekTurn(turnNum);
|
||||
this.update(null);
|
||||
},
|
||||
'switchsides'() {
|
||||
if (!this.battle) return this.add('|error|You are not in a battle');
|
||||
this.battle.switchViewpoint();
|
||||
},
|
||||
'cancel,undo'() {
|
||||
if (!this.battle) return this.send('/cancelchallenge');
|
||||
|
||||
const room = this as any as BattleRoom;
|
||||
if (!room.choices || !room.request) {
|
||||
this.receiveLine([`error`, `/choose - You are not a player in this battle`]);
|
||||
return;
|
||||
}
|
||||
if (room.choices.isDone() || room.choices.isEmpty()) {
|
||||
this.sendDirect('/undo');
|
||||
}
|
||||
room.choices = new BattleChoiceBuilder(room.request);
|
||||
this.update(null);
|
||||
},
|
||||
'move,switch,team,pass,shift,choose'(target, cmd) {
|
||||
if (!this.battle) return this.add('|error|You are not in a battle');
|
||||
const room = this as any as BattleRoom;
|
||||
if (!room.choices) {
|
||||
this.receiveLine([`error`, `/choose - You are not a player in this battle`]);
|
||||
return;
|
||||
}
|
||||
if (cmd !== 'choose') target = `${cmd} ${target}`;
|
||||
const possibleError = room.choices.addChoice(target);
|
||||
if (possibleError) {
|
||||
this.receiveLine([`error`, possibleError]);
|
||||
return;
|
||||
}
|
||||
if (room.choices.isDone()) this.sendDirect(`/choose ${room.choices.toString()}`);
|
||||
this.update(null);
|
||||
},
|
||||
});
|
||||
openChallenge() {
|
||||
if (!this.pmTarget) {
|
||||
this.receiveLine([`error`, `Can only be used in a PM.`]);
|
||||
this.add(`|error|Can only be used in a PM.`);
|
||||
return;
|
||||
}
|
||||
this.challengeMenuOpen = true;
|
||||
|
|
@ -307,7 +329,7 @@ export class ChatRoom extends PSRoom {
|
|||
}
|
||||
cancelChallenge() {
|
||||
if (!this.pmTarget) {
|
||||
this.receiveLine([`error`, `Can only be used in a PM.`]);
|
||||
this.add(`|error|Can only be used in a PM.`);
|
||||
return;
|
||||
}
|
||||
if (this.challenging) {
|
||||
|
|
@ -381,18 +403,22 @@ export class ChatRoom extends PSRoom {
|
|||
this.update(null);
|
||||
}
|
||||
addUser(username: string) {
|
||||
if (!username) return;
|
||||
|
||||
const userid = toID(username);
|
||||
if (!(userid in this.users)) this.userCount++;
|
||||
this.users[userid] = username;
|
||||
this.update(null);
|
||||
}
|
||||
removeUser(username: string, noUpdate?: boolean) {
|
||||
if (!username) return;
|
||||
|
||||
const userid = toID(username);
|
||||
if (userid in this.users) {
|
||||
this.userCount--;
|
||||
delete this.users[userid];
|
||||
if (!noUpdate) this.update(null);
|
||||
}
|
||||
if (!noUpdate) this.update(null);
|
||||
}
|
||||
renameUser(username: string, oldUsername: string) {
|
||||
this.removeUser(oldUsername, true);
|
||||
|
|
@ -401,43 +427,37 @@ export class ChatRoom extends PSRoom {
|
|||
}
|
||||
|
||||
handleJoinLeave(action: 'join' | 'leave', name: string, silent: boolean) {
|
||||
if (!this.joinLeave) {
|
||||
this.joinLeave = {
|
||||
join: [],
|
||||
leave: [],
|
||||
messageId: 'joinleave-' + String(Date.now()),
|
||||
};
|
||||
}
|
||||
if (!action) return;
|
||||
if (action === 'join') {
|
||||
this.addUser(name);
|
||||
} else if (action === 'leave') {
|
||||
this.removeUser(name);
|
||||
}
|
||||
let allShowjoins = PS.prefs.showjoins || {};
|
||||
let showjoins = allShowjoins[PS.server.id];
|
||||
if (silent && (!showjoins || (!showjoins['global'] && !showjoins[this.id]) || showjoins[this.id] === 0)) {
|
||||
return;
|
||||
}
|
||||
let formattedUser = name;
|
||||
if (action === 'join' && this.joinLeave['leave'].includes(formattedUser)) {
|
||||
this.joinLeave['leave'].splice(this.joinLeave['leave'].indexOf(formattedUser), 1);
|
||||
const showjoins = PS.prefs.showjoins?.[PS.server.id];
|
||||
if (!(showjoins?.[this.id] ?? showjoins?.['global'] ?? !silent)) return;
|
||||
|
||||
this.joinLeave ||= {
|
||||
join: [],
|
||||
leave: [],
|
||||
messageId: `joinleave-${Date.now()}`,
|
||||
};
|
||||
if (action === 'join' && this.joinLeave['leave'].includes(name)) {
|
||||
this.joinLeave['leave'].splice(this.joinLeave['leave'].indexOf(name), 1);
|
||||
} else if (action === 'leave' && this.joinLeave['join'].includes(name)) {
|
||||
this.joinLeave['join'].splice(this.joinLeave['join'].indexOf(name), 1);
|
||||
} else {
|
||||
this.joinLeave[action].push(formattedUser);
|
||||
this.joinLeave[action].push(name);
|
||||
}
|
||||
|
||||
let message = '';
|
||||
if (this.joinLeave['join'].length) {
|
||||
message += this.formatJoinLeave(this.joinLeave['join'], 'joined');
|
||||
}
|
||||
if (this.joinLeave['leave'].length) {
|
||||
if (this.joinLeave['join'].length) message += '; ';
|
||||
message += this.formatJoinLeave(this.joinLeave['leave'], 'left') + '<br />';
|
||||
}
|
||||
this.receiveLine(['uhtml', this.joinLeave.messageId, `<small style="color: #555555"> ${message} </small>`]);
|
||||
let message = this.formatJoinLeave(this.joinLeave['join'], 'joined');
|
||||
if (this.joinLeave['join'].length && this.joinLeave['leave'].length) message += '; ';
|
||||
message += this.formatJoinLeave(this.joinLeave['leave'], 'left');
|
||||
|
||||
this.add(`|uhtml|${this.joinLeave.messageId}|<small style="color: #555555">${message}</small>`);
|
||||
}
|
||||
|
||||
formatJoinLeave(preList: string[], action: 'joined' | 'left') {
|
||||
if (!preList.length) return '';
|
||||
|
||||
let message = '';
|
||||
let list: string[] = [];
|
||||
let named: { [key: string]: boolean } = {};
|
||||
|
|
@ -461,7 +481,7 @@ export class ChatRoom extends PSRoom {
|
|||
}
|
||||
message += BattleLog.escapeHTML(list[j]);
|
||||
}
|
||||
return message + ' ' + action;
|
||||
return `${message} ${action}`;
|
||||
}
|
||||
|
||||
override destroy() {
|
||||
|
|
|
|||
|
|
@ -189,7 +189,7 @@ class LadderFormatPanel extends PSRoomPanel<LadderFormatRoom> {
|
|||
{row.username}
|
||||
</span></td>
|
||||
<td style={{ textAlign: 'center' }}><strong>{row.elo.toFixed(0)}</strong></td>
|
||||
<td style={{ textAlign: 'center' }}>{row.gxe.toFixed(1)}<small>%</small></td>
|
||||
<td style={{ textAlign: 'center' }}>{Math.trunc(row.gxe)}<small>.{row.gxe.toFixed(1).slice(-1)}%</small></td>
|
||||
<td style={{ textAlign: 'center' }}><em>{row.rpr.toFixed(0)}<small> ± {row.rprd.toFixed(0)}</small></em></td>
|
||||
{showCOIL && <td style={{ textAlign: 'center' }}>{row.coil?.toFixed(0)}</td>}
|
||||
</tr>)}
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ export class MainMenuRoom extends PSRoom {
|
|||
}
|
||||
PS.teams.update('format');
|
||||
}
|
||||
handlePM(user1: string, user2: string, message: string) {
|
||||
handlePM(user1: string, user2: string, message?: string) {
|
||||
const userid1 = toID(user1);
|
||||
const userid2 = toID(user2);
|
||||
const pmTarget = PS.user.userid === userid1 ? user2 : user1;
|
||||
|
|
@ -314,7 +314,7 @@ export class MainMenuRoom extends PSRoom {
|
|||
} else {
|
||||
room.updateTarget(pmTarget);
|
||||
}
|
||||
room.receiveLine([`c`, user1, message]);
|
||||
if (message) room.receiveLine([`c`, user1, message]);
|
||||
PS.update();
|
||||
}
|
||||
handleQueryResponse(id: ID, response: any) {
|
||||
|
|
|
|||
|
|
@ -23,37 +23,25 @@ class TeambuilderRoom extends PSRoom {
|
|||
curFolder = '';
|
||||
curFolderKeep = '';
|
||||
|
||||
/**
|
||||
* @return true to prevent line from being sent to server
|
||||
*/
|
||||
override handleSend(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 'newteam': {
|
||||
override clientCommands = this.parseClientCommands({
|
||||
'newteam'(target) {
|
||||
if (target === 'bottom') {
|
||||
PS.teams.push(this.createTeam());
|
||||
} else {
|
||||
PS.teams.unshift(this.createTeam());
|
||||
}
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'deleteteam': {
|
||||
},
|
||||
'deleteteam'(target) {
|
||||
const team = PS.teams.byKey[target];
|
||||
if (team) PS.teams.delete(team);
|
||||
this.update(null);
|
||||
return true;
|
||||
} case 'undeleteteam': {
|
||||
},
|
||||
'undeleteteam'() {
|
||||
PS.teams.undelete();
|
||||
this.update(null);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return super.handleSend(line);
|
||||
}
|
||||
},
|
||||
});
|
||||
override sendDirect(msg: string): void {
|
||||
PS.alert(`Unrecognized command: ${msg}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -601,6 +601,9 @@ input[type=range]:active::-webkit-slider-thumb {
|
|||
.chat small {
|
||||
font-weight: normal;
|
||||
font-size: 8pt;
|
||||
}
|
||||
.chat strong small, /* <- for old client */
|
||||
.chat .groupsymbol {
|
||||
color: #888888;
|
||||
}
|
||||
.chat.timer {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user