Preact minor updates batch 17

Teambuilder
- Show "EVs" when a set has no EVs, so it's clear what the Stats button
  is for
- Fix dragging slider clearing Nature
- Fix teams not being clickable on iOS
- Minor layout tweaks
- Use four moves on compat mode exports, too

Minor
- Add a "PSIcon" component
- Update README
- Fix Team Preview in doubles/etc
- Fix move choice preview
- Fix `/avatar`
- Fix display of formats with custom rules in the format dropdown
- Support "Register" button from winning a battle
- Support "More" button from `/rank`
- Fix target choosing in multi battles
- Fix switching in more slots than unfainted pokemon (in doubles+)
- Add `/senddirect` command to bypass client command parser
- Fix notifications for mini-rooms (they should highlight the mainmenu
  tab)
- Set tooltip long-tap delay to 1 second, to make it harder to
  accidentally long-tap

Trivial
- move FormatResource stuff out of teamdropdown
- refactor battle-choices a little
- make team tabs less tall in teambuilder wizard individual set view
- support `/choose default` or `/choose auto`
This commit is contained in:
Guangcong Luo 2025-05-05 09:19:14 +00:00
parent 6c80aa25a2
commit e5c25bbb97
15 changed files with 273 additions and 123 deletions

View File

@ -39,11 +39,20 @@ Pokémon Showdown is usable, but expect degraded performance and certain feature
Pokémon Showdown is mostly developed on Chrome, and Chrome or the desktop client is required for certain features like dragging-and-dropping teams from PS to your computer. However, bugs reported on any supported browser will usually be fixed pretty quickly.
Testing
New client
------------------------------------------------------------------------
Client testing now requires a build step! Install the latest Node.js (we
require v14 or later) and Git, and run `node build` (on Windows) or `./build`
Development is proceeding on the new Preact client! The live version is
available at https://play.pokemonshowdown.com/preactalpha
You can contribute to it yourself using the same process as before, just
use `testclient-beta.html` rather than `testclient.html`.
Testing (the old client)
------------------------------------------------------------------------
Client testing requires a build step! Install the latest Node.js (we
require v20 or later) and Git, and run `node build` (on Windows) or `./build`
(on other OSes) to build.
You can make and test client changes simply by building after each change,

View File

@ -61,6 +61,7 @@ export interface BattleMoveRequest {
side: BattleRequestSideInfo;
active: (BattleRequestActivePokemon | null)[];
noCancel?: boolean;
targetable?: boolean;
}
export interface BattleSwitchRequest {
requestType: 'switch';
@ -74,6 +75,8 @@ export interface BattleTeamRequest {
rqid: number;
side: BattleRequestSideInfo;
maxTeamSize?: number;
maxChosenTeamSize?: number;
chosenTeamSize?: number;
noCancel?: boolean;
}
export interface BattleWaitRequest {
@ -166,7 +169,7 @@ export class BattleChoiceBuilder {
}
/** Index of the current Pokémon to make choices for */
index() {
index(): number {
return this.choices.length;
}
/** How many choices is the server expecting? */
@ -178,15 +181,24 @@ export class BattleChoiceBuilder {
case 'switch':
return request.forceSwitch.length;
case 'team':
if (request.maxTeamSize) return request.maxTeamSize;
return 1;
return request.chosenTeamSize || 1;
case 'wait':
return 0;
}
}
currentMoveRequest() {
currentMoveRequest(index = this.index()) {
if (this.request.requestType !== 'move') return null;
return this.request.active[this.index()];
return this.request.active[index];
}
noMoreSwitchChoices() {
if (this.request.requestType !== 'switch') return false;
for (let i = this.requestLength(); i < this.request.side.pokemon.length; i++) {
const pokemon = this.request.side.pokemon[i];
if (!pokemon.fainted && !this.alreadySwitchingIn.includes(i + 1)) {
return false;
}
}
return true;
}
addChoice(choiceString: string) {
@ -202,15 +214,10 @@ export class BattleChoiceBuilder {
/** only the last choice can be uncancelable */
const isLastChoice = this.choices.length + 1 >= this.requestLength();
if (choice.choiceType === 'move') {
if (!choice.targetLoc && this.requestLength() > 1) {
const choosableTargets = ['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe'];
if (choosableTargets.includes(this.getChosenMove(choice, this.index()).target)) {
this.current.move = choice.move;
this.current.mega = choice.mega;
this.current.ultra = choice.ultra;
this.current.z = choice.z;
this.current.max = choice.max;
this.current.tera = choice.tera;
if (!choice.targetLoc && (this.request as BattleMoveRequest).targetable) {
const choosableTargets: unknown[] = ['normal', 'any', 'adjacentAlly', 'adjacentAllyOrSelf', 'adjacentFoe'];
if (choosableTargets.includes(this.currentMove(choice)?.target)) {
this.current = choice;
return null;
}
}
@ -221,12 +228,18 @@ export class BattleChoiceBuilder {
if (choice.z) this.alreadyZ = true;
if (choice.max) this.alreadyMax = true;
if (choice.tera) this.alreadyTera = true;
this.current.move = 0;
this.current.mega = false;
this.current.ultra = false;
this.current.z = false;
this.current.max = false;
this.current.tera = false;
this.current = {
choiceType: 'move',
move: 0,
targetLoc: 0,
mega: false,
megax: false,
megay: false,
ultra: false,
z: false,
max: false,
tera: false,
};
} else if (choice.choiceType === 'switch' || choice.choiceType === 'team') {
if (this.currentMoveRequest()?.trapped) {
return "You are trapped and cannot switch out";
@ -277,27 +290,26 @@ export class BattleChoiceBuilder {
}
break;
case 'switch':
while (this.choices.length < request.forceSwitch.length && !request.forceSwitch[this.choices.length]) {
this.choices.push('pass');
const noMoreSwitchChoices = this.noMoreSwitchChoices();
while (this.choices.length < request.forceSwitch.length) {
if (!request.forceSwitch[this.choices.length] || noMoreSwitchChoices) {
this.choices.push('pass');
} else {
break;
}
}
}
}
getChosenMove(choice: BattleMoveChoice, pokemonIndex: number) {
const request = this.request as BattleMoveRequest;
const activePokemon = request.active[pokemonIndex]!;
currentMove(choice = this.current, index = this.index()) {
const moveIndex = choice.move - 1;
if (choice.z) {
return activePokemon.zMoves![moveIndex]!;
}
if (choice.max || (activePokemon.maxMoves && !activePokemon.canDynamax)) {
return activePokemon.maxMoves![moveIndex];
}
return activePokemon.moves[moveIndex];
return this.currentMoveList(index, choice)?.[moveIndex] || null;
}
currentMoveList(current: { max?: boolean, z?: boolean } = this.current) {
const moveRequest = this.currentMoveRequest();
currentMoveList(
index = this.index(), current: { max?: boolean, z?: boolean } = this.current
): ({ name: string, id: ID, target: Dex.MoveTarget, disabled?: boolean } | null)[] | null {
const moveRequest = this.currentMoveRequest(index);
if (!moveRequest) return null;
if (current.max || (moveRequest.maxMoves && !moveRequest.canDynamax)) {
return moveRequest.maxMoves || null;
@ -310,12 +322,10 @@ export class BattleChoiceBuilder {
/**
* Parses a choice from string form to BattleChoice form
*/
parseChoice(choice: string): BattleChoice | null {
parseChoice(choice: string, index = this.choices.length): BattleChoice | null {
const request = this.request;
if (request.requestType === 'wait') throw new Error(`It's not your turn to choose anything`);
const index = this.choices.length;
if (choice === 'shift' || choice === 'testfight') {
if (request.requestType !== 'move') {
throw new Error(`You must switch in a Pokémon, not move.`);
@ -385,10 +395,6 @@ export class BattleChoiceBuilder {
if (/^[0-9]+$/.test(choice)) {
// Parse a one-based move index.
current.move = parseInt(choice, 10);
const move = this.currentMoveList()?.[current.move - 1];
if (!move || move.disabled) {
throw new Error(`Move ${move?.name ?? current.move} is disabled`);
}
} else {
// Parse a move ID.
// Move names are also allowed, but may cause ambiguity (see client issue #167).
@ -428,6 +434,10 @@ export class BattleChoiceBuilder {
}
}
if (current.max && !moveRequest.canDynamax) current.max = false;
const move = this.currentMove(current, index);
if (!move || move.disabled) {
throw new Error(`Move ${move?.name ?? current.move} is disabled`);
}
return current;
}
@ -545,6 +555,31 @@ export class BattleChoiceBuilder {
battle.parseHealth(serverPokemon.condition, serverPokemon);
}
}
if (request.requestType === 'team' && !request.chosenTeamSize) {
request.chosenTeamSize = 1;
if (battle.gameType === 'doubles') {
request.chosenTeamSize = 2;
}
if (battle.gameType === 'triples' || battle.gameType === 'rotation') {
request.chosenTeamSize = 3;
}
// Request full team order if one of our Pokémon has Illusion
for (const switchable of request.side.pokemon) {
if (toID(switchable.baseAbility) === 'illusion') {
request.chosenTeamSize = request.side.pokemon.length;
}
}
if (request.maxChosenTeamSize) {
request.chosenTeamSize = request.maxChosenTeamSize;
}
if (battle.teamPreviewCount) {
const chosenTeamSize = battle.teamPreviewCount;
if (chosenTeamSize > 0 && chosenTeamSize <= request.side.pokemon.length) {
request.chosenTeamSize = chosenTeamSize;
}
}
}
request.targetable ||= battle.mySide.active.length > 1;
if (request.active) {
request.active = request.active.map(

View File

@ -1533,13 +1533,14 @@ class StatForm extends preact.Component<{
if (statID === 'spd' && editor.gen === 1) return null;
const stat = editor.getStat(statID, set);
const ev = set.evs?.[statID] ?? defaultEV;
let ev: number | string = set.evs?.[statID] ?? defaultEV;
let width = stat * 75 / 504;
if (statID === 'hp') width = stat * 75 / 704;
if (width > 75) width = 75;
let hue = Math.floor(stat * 180 / 714);
if (hue > 360) hue = 360;
const statName = editor.gen === 1 && statID === 'spa' ? 'Spc' : BattleStatNames[statID];
if (evs && !ev && !set.evs && statID === 'hp') ev = 'EVs';
return <span class="statrow">
<label>{statName}</label> {}
<span class="statgraph">
@ -1807,30 +1808,13 @@ class StatForm extends preact.Component<{
const statID = target.name.split('-')[1] as Dex.StatName;
let value = Math.abs(parseInt(target.value));
if (target.value.includes('+')) {
if (statID === 'hp') {
alert("Natures cannot raise or lower HP.");
return;
}
this.plus = statID;
} else if (this.plus === statID) {
this.plus = null;
}
if (target.value.includes('-')) {
if (statID === 'hp') {
alert("Natures cannot raise or lower HP.");
return;
}
this.minus = statID;
} else if (this.minus === statID) {
this.minus = null;
}
if (isNaN(value)) {
if (set.evs) delete set.evs[statID];
} else {
set.evs ||= {};
set.evs[statID] = value;
}
if (target.type === 'range') {
// enforce limit
const maxEv = this.maxEVs();
@ -1841,9 +1825,28 @@ class StatForm extends preact.Component<{
set.evs![statID] = maxEv - (totalEv - value) - (maxEv % 4);
}
}
} else {
if (target.value.includes('+')) {
if (statID === 'hp') {
alert("Natures cannot raise or lower HP.");
return;
}
this.plus = statID;
} else if (this.plus === statID) {
this.plus = null;
}
if (target.value.includes('-')) {
if (statID === 'hp') {
alert("Natures cannot raise or lower HP.");
return;
}
this.minus = statID;
} else if (this.minus === statID) {
this.minus = null;
}
this.updateNatureFromPlusMinus();
}
this.updateNatureFromPlusMinus();
this.props.onChange();
};
updateNatureFromPlusMinus = () => {

View File

@ -156,7 +156,7 @@ export class BattleTooltips {
// tooltips
// Touch delay, pressing finger more than that time will cause the tooltip to open.
// Shorter time will cause the button to click
static LONG_TAP_DELAY = 350; // ms
static LONG_TAP_DELAY = 1000; // ms
static longTapTimeout = 0;
static elem: HTMLDivElement | null = null;
static parentElem: HTMLElement | null = null;
@ -243,7 +243,8 @@ export class BattleTooltips {
if (BattleTooltips.isLocked) BattleTooltips.hideTooltip();
const target = e.currentTarget as HTMLElement;
this.showTooltip(target);
let factor = (e.type === 'mousedown' && target.tagName === 'BUTTON' ? 2 : 1);
// let factor = (e.type === 'mousedown' && target.tagName === 'BUTTON' ? 2 : 1);
const factor = 1;
BattleTooltips.longTapTimeout = setTimeout(() => {
BattleTooltips.longTapTimeout = 0;
@ -787,10 +788,6 @@ export class BattleTooltips {
*
* isActive is true if hovering over a pokemon in the battlefield,
* and false if hovering over a pokemon in the Switch menu.
*
* @param clientPokemon
* @param serverPokemon
* @param isActive
*/
showPokemonTooltip(
clientPokemon: Pokemon | null, serverPokemon?: ServerPokemon | null, isActive?: boolean, illusionIndex?: number

View File

@ -1101,7 +1101,7 @@ export class Battle {
teamPreviewCount = 0;
speciesClause = false;
tier = '';
gameType: 'singles' | 'doubles' | 'triples' | 'multi' | 'freeforall' = 'singles';
gameType: 'singles' | 'doubles' | 'triples' | 'multi' | 'freeforall' | 'rotation' = 'singles';
compatMode = true;
rated: string | boolean = false;
rules: { [ruleName: string]: 1 | undefined } = {};

View File

@ -999,12 +999,14 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
}
},
'avatar'(target) {
const avatar = window.BattleAvatarNumbers?.[toID(target)] || toID(target);
target = target.toLowerCase();
if (/[^a-z0-9-]/.test(target)) target = toID(target);
const avatar = window.BattleAvatarNumbers?.[target] || target;
PS.user.avatar = avatar;
if (this.type !== 'chat' && this.type !== 'battle') {
PS.send(`|/avatar ${avatar}`);
} else {
this.send(`/avatar ${avatar}`);
this.sendDirect(`/avatar ${avatar}`);
}
},
'open,user'(target) {
@ -1185,6 +1187,9 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
}
this.add("||All PM windows cleared and closed.");
},
'senddirect'(target) {
this.sendDirect(target);
},
'help'(target) {
switch (toID(target)) {
case 'chal':

View File

@ -7,7 +7,7 @@
import preact from "../js/lib/preact";
import { PS, PSRoom, type RoomOptions, type RoomID } from "./client-main";
import { PSPanelWrapper, PSRoomPanel } from "./panels";
import { PSIcon, PSPanelWrapper, PSRoomPanel } from "./panels";
import { ChatLog, ChatRoom, ChatTextEntry, ChatUserList } from "./panel-chat";
import { FormatDropdown } from "./panel-mainmenu";
import { Battle, type Pokemon, type ServerPokemon } from "./battle";
@ -187,7 +187,7 @@ function PokemonButton(props: {
data-cmd={props.cmd} class={`${props.disabled ? 'disabled ' : ''}has-tooltip`}
style={{ opacity: props.disabled === 'fade' ? 0.5 : 1 }} data-tooltip={props.tooltip}
>
<span class="picon" style={Dex.getPokemonIcon(pokemon)}></span>
<PSIcon pokemon={pokemon} />
{pokemon.name}
{
!props.noHPBar && !pokemon.fainted &&
@ -541,7 +541,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
}
renderMoveTargetControls(request: BattleMoveRequest, choices: BattleChoiceBuilder) {
const battle = this.props.room.battle;
const moveTarget = choices.getChosenMove(choices.current, choices.index()).target;
const moveTarget = choices.currentMove()?.target;
const moveChoice = choices.stringChoice(choices.current);
const userSlot = choices.index();
@ -634,7 +634,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
}
renderOldChoices(request: BattleRequest, choices: BattleChoiceBuilder) {
if (!choices) return null; // should not happen
if (request.requestType !== 'move' && request.requestType !== 'switch') return;
if (request.requestType !== 'move' && request.requestType !== 'switch' && request.requestType !== 'team') return;
if (choices.isEmpty()) return null;
let buf: preact.ComponentChild[] = [
@ -653,7 +653,12 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
buf.push(`${request.side.pokemon[i].name} is locked into a move.`);
return buf;
}
const choice = choices.parseChoice(choiceString);
let choice;
try {
choice = choices.parseChoice(choiceString, i);
} catch (err: any) {
buf.push(<span class="message-error">{err.message}</span>);
}
if (!choice) continue;
const pokemon = request.side.pokemon[i];
const active = request.requestType === 'move' ? request.active[i] : null;
@ -665,7 +670,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
if (choice.ultra) buf.push(<strong>Ultra</strong>, ` Burst and `);
if (choice.tera) buf.push(`Terastallize (`, <strong>{active?.canTerastallize || '???'}</strong>, `) and `);
if (choice.max && active?.canDynamax) buf.push(active?.canGigantamax ? `Gigantamax and ` : `Dynamax and `);
buf.push(`use `, <strong>{choices.getChosenMove(choice, i).name}</strong>);
buf.push(`use `, <strong>{choices.currentMove(choice, i)?.name}</strong>);
if (choice.targetLoc > 0) {
const target = battle.farSide.active[choice.targetLoc - 1];
if (!target) {
@ -686,6 +691,9 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
buf.push(`${pokemon.name} will switch to `, <strong>{target.name}</strong>);
} else if (choice.choiceType === 'shift') {
buf.push(`${pokemon.name} will `, <strong>shift</strong>, ` to the center`);
} else if (choice.choiceType === 'team') {
const target = request.side.pokemon[choice.targetPokemon - 1];
buf.push(`You picked `, <strong>{target.name}</strong>);
}
buf.push(<br />);
}
@ -718,7 +726,7 @@ class BattlePanel extends PSRoomPanel<BattleRoom> {
const pokemon = request.side.pokemon[index];
if (choices.current.move) {
const moveName = choices.getChosenMove(choices.current, choices.index()).name;
const moveName = choices.currentMove()?.name;
return <div class="controls">
<div class="whatdo">
{this.renderOldChoices(request, choices)}

View File

@ -403,6 +403,10 @@ export class ChatRoom extends PSRoom {
return;
}
if (cmd !== 'choose') target = `${cmd} ${target}`;
if (target === 'choose auto' || target === 'choose default') {
this.sendDirect('/choose default');
return;
}
const possibleError = room.choices.addChoice(target);
if (possibleError) {
this.errorReply(possibleError);

View File

@ -8,7 +8,7 @@
import preact from "../js/lib/preact";
import { PSLoginServer } from "./client-connection";
import { PS, PSRoom, type RoomID, type RoomOptions, type Team } from "./client-main";
import { PSPanelWrapper, PSRoomPanel } from "./panels";
import { PSIcon, PSPanelWrapper, PSRoomPanel } from "./panels";
import type { BattlesRoom } from "./panel-battle";
import type { ChatRoom } from "./panel-chat";
import type { LadderFormatRoom } from "./panel-ladder";
@ -653,7 +653,7 @@ export class FormatDropdown extends preact.Component<{
}
render() {
let [formatName, customRules] = this.format.split('@@@');
if (window.BattleLog) formatName = BattleLog.formatName(this.format);
if (window.BattleLog) formatName = BattleLog.formatName(formatName);
if (this.props.format && !this.props.onChange) {
return <button
name="format" value={this.format} class="select formatselect preselected" disabled
@ -694,12 +694,12 @@ class TeamDropdown extends preact.Component<{ format: string }> {
<div class="team">
<strong>Random team</strong>
<small>
<span class="picon" style={Dex.getPokemonIcon(null)}></span>
<span class="picon" style={Dex.getPokemonIcon(null)}></span>
<span class="picon" style={Dex.getPokemonIcon(null)}></span>
<span class="picon" style={Dex.getPokemonIcon(null)}></span>
<span class="picon" style={Dex.getPokemonIcon(null)}></span>
<span class="picon" style={Dex.getPokemonIcon(null)}></span>
<PSIcon pokemon={null} />
<PSIcon pokemon={null} />
<PSIcon pokemon={null} />
<PSIcon pokemon={null} />
<PSIcon pokemon={null} />
<PSIcon pokemon={null} />
</small>
</div>
</button>;

View File

@ -7,11 +7,11 @@
import { PS, PSRoom, type RoomOptions, type Team } from "./client-main";
import { PSPanelWrapper, PSRoomPanel } from "./panels";
import { PSTeambuilder, type FormatResource } from "./panel-teamdropdown";
import { toID } from "./battle-dex";
import { BattleLog } from "./battle-log";
import { FormatDropdown } from "./panel-mainmenu";
import { TeamEditor } from "./battle-team-editor";
import { Net } from "./client-connection";
class TeamRoom extends PSRoom {
/** Doesn't _literally_ always exist, but does in basically all code
@ -38,6 +38,7 @@ class TeamRoom extends PSRoom {
}
}
export type FormatResource = { url: string, resources: { resource_name: string, url: string }[] } | null;
class TeamPanel extends PSRoomPanel<TeamRoom> {
static readonly id = 'team';
static readonly routes = ['team-*'];
@ -50,7 +51,7 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
super(props);
const room = this.props.room;
if (room.team) {
PSTeambuilder.getFormatResources(room.team.format).then(resources => {
TeamPanel.getFormatResources(room.team.format).then(resources => {
this.resources = resources;
this.forceUpdate();
});
@ -59,6 +60,20 @@ class TeamPanel extends PSRoomPanel<TeamRoom> {
}
}
static formatResources = {} as Record<string, FormatResource>;
static getFormatResources(format: string): Promise<FormatResource> {
if (format in this.formatResources) return Promise.resolve(this.formatResources[format]);
return Net('https://www.smogon.com/dex/api/formats/by-ps-name/' + format).get()
.then(result => {
this.formatResources[format] = JSON.parse(result);
return this.formatResources[format];
}).catch(err => {
this.formatResources[format] = null;
return this.formatResources[format];
});
}
handleRename = (ev: Event) => {
const textbox = ev.currentTarget as HTMLInputElement;
const room = this.props.room;

View File

@ -9,9 +9,6 @@ import { PS, type Team } from "./client-main";
import { PSPanelWrapper, PSRoomPanel } from "./panels";
import { Dex, type ModdedDex, toID, type ID } from "./battle-dex";
import { BattleNatures, BattleStatIDs, BattleStatNames, type StatNameExceptHP } from "./battle-dex-data";
import { Net } from "./client-connection";
export type FormatResource = { url: string, resources: { resource_name: string, url: string }[] } | null;
export class PSTeambuilder {
static packTeam(team: Dex.PokemonSet[]) {
@ -302,16 +299,17 @@ export class PSTeambuilder {
text += `Tera Type: ${set.teraType}\n`;
}
if (set.moves && compat) {
for (let move of set.moves) {
if (compat) {
for (let move of set.moves || []) {
if (move.startsWith('Hidden Power ')) {
const hpType = move.slice(13);
move = move.slice(0, 13);
move = compat ? `${move}[${hpType}]` : `${move}${hpType}`;
}
if (move) {
text += `- ${move}\n`;
}
text += `- ${move}\n`;
}
for (let i = set.moves?.length || 0; i < 4; i++) {
text += `- \n`;
}
}
@ -586,20 +584,6 @@ export class PSTeambuilder {
return team;
}
static formatResources = {} as Record<string, FormatResource>;
static getFormatResources(format: string): Promise<FormatResource> {
if (format in this.formatResources) return Promise.resolve(this.formatResources[format]);
return Net('https://www.smogon.com/dex/api/formats/by-ps-name/' + format).get()
.then(result => {
this.formatResources[format] = JSON.parse(result);
return this.formatResources[format];
}).catch(err => {
this.formatResources[format] = null;
return this.formatResources[format];
});
}
}
export function TeamFolder(props: { cur?: boolean, value: string, children: preact.ComponentChildren }) {
@ -626,6 +610,7 @@ export function TeamBox(props: { team: Team | null, noLink?: boolean, button?: b
icons = <em>(empty team)</em>;
} else {
icons = PSTeambuilder.packedTeamNames(team.packedTeam).map(species =>
// can't use PSIcon, weird interaction with iconCache
<span class="picon" style={Dex.getPokemonIcon(species)}></span>
);
}
@ -648,9 +633,14 @@ export function TeamBox(props: { team: Team | null, noLink?: boolean, button?: b
{contents}
</button>;
}
return <div data-href={props.noLink ? '' : `/team-${team ? team.key : ''}`} class="team" draggable>
if (props.noLink) {
return <div class="team">
{contents}
</div>;
}
return <a href={`team-${team ? team.key : ''}`} class="team" draggable>
{contents}
</div>;
</a>;
}
/**

View File

@ -131,7 +131,13 @@ export class PSHeader extends preact.Component<{ style: object }> {
const cur = PS.isVisible(room) ? ' cur' : '';
let notifying = room.isSubtleNotifying ? ' subtle-notifying' : '';
let hoverTitle = '';
const notifications = room.notifications;
let notifications = room.notifications;
if (id === '') {
for (const roomid of PS.miniRoomList) {
const miniNotifications = PS.rooms[roomid]?.notifications;
if (miniNotifications?.length) notifications = [...notifications, ...miniNotifications];
}
}
if (notifications.length) {
notifying = ' notifying';
for (const notif of notifications) {

View File

@ -10,12 +10,14 @@
*/
import preact from "../js/lib/preact";
import { toID } from "./battle-dex";
import type { Pokemon, ServerPokemon } from "./battle";
import { Dex, toID } from "./battle-dex";
import type { Args } from "./battle-text-parser";
import { BattleTooltips } from "./battle-tooltips";
import { Net } from "./client-connection";
import type { PSModel, PSStreamModel, PSSubscription } from "./client-core";
import { PS, type PSRoom, type RoomID } from "./client-main";
import type { ChatRoom } from "./panel-chat";
import { PSHeader, PSMiniHeader } from "./panel-topbar";
export class PSRouter {
@ -609,6 +611,27 @@ export class PSView extends preact.Component {
parentElem: elem,
});
return true;
case 'register':
PS.join('register' as RoomID, {
parentElem: elem,
});
return true;
case 'showOtherFormats': {
// TODO: refactor to a command after we drop support for the old client
const table = elem.closest('table');
const room = PS.getRoom(elem);
if (table) {
for (const row of table.querySelectorAll<HTMLElement>('tr.hidden')) {
row.style.display = 'table-row';
}
for (const row of table.querySelectorAll<HTMLElement>('tr.no-matches')) {
row.style.display = 'none';
}
elem.closest('tr')!.style.display = 'none';
(room as ChatRoom).log?.updateScroll();
}
return true;
}
case 'copyText':
const dummyInput = document.createElement("input");
// This is a hack. You can only "select" an input field.
@ -804,3 +827,43 @@ export class PSView extends preact.Component {
</div>;
}
}
export function PSIcon(
props: { pokemon: string | Pokemon | ServerPokemon | Dex.PokemonSet | null } |
{ item: string } | { type: string, b?: boolean } | { category: string }
) {
if ('pokemon' in props) {
return <span class="picon" style={Dex.getPokemonIcon(props.pokemon)} />;
}
if ('item' in props) {
return <span class="itemicon" style={Dex.getItemIcon(props.item)} />;
}
if ('type' in props) {
let type = Dex.types.get(props.type).name;
if (!type) type = '???';
let sanitizedType = type.replace(/\?/g, '%3f');
return <img
src={`${Dex.resourcePrefix}sprites/types/${sanitizedType}.png`} alt={type}
height="14" width="32" class={`pixelated${props.b ? ' b' : ''}`}
/>;
}
if ('category' in props) {
const categoryID = toID(props.category);
let sanitizedCategory = '';
switch (categoryID) {
case 'physical':
case 'special':
case 'status':
sanitizedCategory = categoryID.charAt(0).toUpperCase() + categoryID.slice(1);
break;
default:
sanitizedCategory = 'undefined';
break;
}
return <img
src={`${Dex.resourcePrefix}sprites/categories/${sanitizedCategory}.png`} alt={sanitizedCategory}
height="14" width="32" class="pixelated"
/>;
}
return null!;
}

View File

@ -2014,6 +2014,7 @@ pre.textbox.textbox-empty[placeholder]:before {
font-size: 9pt;
text-align: left;
font-family: Verdana, Helvetica, Arial, sans-serif;
text-decoration: none;
white-space: nowrap;
cursor: pointer;

View File

@ -298,6 +298,7 @@
.teameditor {
padding-bottom: 30px;
max-width: 660px;
}
.teameditor-text {
position: relative;
@ -553,10 +554,13 @@ you can't delete it by pressing Backspace */
.set-button .set-nickname {
position: absolute;
height: 35px;
width: 100px;
width: 120px;
top: 5px;
left: -5px;
}
.tiny-layout .set-button .set-nickname {
width: 90px;
}
.set-button .sprite {
display: block;
@ -621,18 +625,28 @@ you can't delete it by pressing Backspace */
overflow: auto;
-webkit-overflow-scrolling: touch;
}
.picontab {
font-size: 10px;
padding: 3px 0;
.team-focus-editor .tabbar {
overflow: auto;
white-space: nowrap;
}
.tabbar .button.picontab {
width: 80px;
height: 52px;
overflow: hidden;
height: 46px;
padding: 0;
font-size: 10px;
box-sizing: border-box;
}
.tiny-layout .picontab {
.tabbar .button.picontab.cur {
height: 47px;
padding: 0 0 2px;
}
.tiny-layout .tabbar .button.picontab {
width: 42px;
}
@media (min-width: 375px) {
.tiny-layout .picontab {
.tiny-layout .tabbar .button.picontab {
width: 52px;
}
}
@ -660,7 +674,7 @@ you can't delete it by pressing Backspace */
}
.wizardsearchresults {
position: absolute;
top: 236px;
top: 230px;
left: 0;
right: 0;
bottom: 0;