From a35a62cfce755a58c29caca5e8a53c2ef7423453 Mon Sep 17 00:00:00 2001
From: Guangcong Luo
Date: Wed, 14 May 2025 14:28:36 +0000
Subject: [PATCH] Preact minor updates batch 20
Battles
- Fix render issues on mobile layout
- Fix join/leave batching
- Support disabled buttons
Teambuilder
- Redesign upload UI to be clearer
- Support scrolling tab bar for Boxes in focus editor
- Don't overwrite local team with remote team
- Improve Defensive Coverage ability support
- Support importing from pokepaste (fixes #2422)
---
play.pokemonshowdown.com/src/battle-log.ts | 2 +-
.../src/battle-team-editor.tsx | 56 ++++-
play.pokemonshowdown.com/src/client-main.ts | 24 +--
play.pokemonshowdown.com/src/panel-battle.tsx | 192 ++++++++++--------
play.pokemonshowdown.com/src/panel-chat.tsx | 6 +-
.../src/panel-teambuilder-team.tsx | 70 ++++---
.../src/panel-teambuilder.tsx | 4 +-
play.pokemonshowdown.com/style/client2.css | 4 +-
.../style/teambuilder.css | 8 +-
9 files changed, 223 insertions(+), 143 deletions(-)
diff --git a/play.pokemonshowdown.com/src/battle-log.ts b/play.pokemonshowdown.com/src/battle-log.ts
index 0f9787665..b8c9899f3 100644
--- a/play.pokemonshowdown.com/src/battle-log.ts
+++ b/play.pokemonshowdown.com/src/battle-log.ts
@@ -141,7 +141,7 @@ export class BattleLog {
let divClass = 'chat';
let divHTML = '';
let noNotify: boolean | undefined;
- if (!['join', 'j', 'leave', 'l'].includes(args[0])) this.joinLeave = null;
+ if (!['join', 'j', 'leave', 'l', 'turn'].includes(args[0])) this.joinLeave = null;
if (!['name', 'n'].includes(args[0])) this.lastRename = null;
switch (args[0]) {
case 'chat': case 'c': case 'c:':
diff --git a/play.pokemonshowdown.com/src/battle-team-editor.tsx b/play.pokemonshowdown.com/src/battle-team-editor.tsx
index 6d851624b..2a5e76cd1 100644
--- a/play.pokemonshowdown.com/src/battle-team-editor.tsx
+++ b/play.pokemonshowdown.com/src/battle-team-editor.tsx
@@ -15,6 +15,7 @@ import { PSSearchResults } from "./battle-searchresults";
import { BattleNatures, BattleStatNames, type StatName } from "./battle-dex-data";
import { BattleStatGuesser, BattleStatOptimizer } from "./battle-tooltips";
import { PSModel } from "./client-core";
+import { Net } from "./client-connection";
type SelectionType = 'pokemon' | 'ability' | 'item' | 'move' | 'stats' | 'details';
@@ -49,6 +50,10 @@ class TeamEditorState extends PSModel {
this.setFormat(team.format);
window.search = this.search;
}
+ setReadonly(readonly: boolean) {
+ if (!readonly && this.readonly) this.sets = PSTeambuilder.unpackTeam(this.team.packedTeam);
+ this.readonly = readonly;
+ }
setFormat(format: string) {
const team = this.team;
const formatid = toID(format);
@@ -499,6 +504,9 @@ class TeamEditorState extends PSModel {
if (attackType === 'Ground' && abilityid === 'eartheater') return 0;
if (attackType === 'Fire' && abilityid === 'wellbakedbody') return 0;
+ if (attackType === 'Fire' && abilityid === 'primordialsea') return 0;
+ if (attackType === 'Water' && abilityid === 'desolateland') return 0;
+
if (abilityid === 'wonderguard') {
for (const type of types) {
if (this.getTypeWeakness(type, attackType) <= 1) return 0;
@@ -506,6 +514,14 @@ class TeamEditorState extends PSModel {
}
let factor = 1;
+ if ((attackType === 'Fire' || attackType === 'Ice') && abilityid === 'thickfat') factor *= 0.5;
+ if (attackType === 'Fire' && abilityid === 'waterbubble') factor *= 0.5;
+ if (attackType === 'Fire' && abilityid === 'heatproof') factor *= 0.5;
+ if (attackType === 'Ghost' && abilityid === 'purifyingsalt') factor *= 0.5;
+ if (attackType === 'Fire' && abilityid === 'fluffy') factor *= 2;
+ if ((attackType === 'Electric' || attackType === 'Rock' || attackType === 'Ice') && abilityid === 'deltastream') {
+ factor *= 0.5;
+ }
for (const type of types) {
factor *= this.getTypeWeakness(type, attackType);
}
@@ -650,7 +666,7 @@ export class TeamEditor extends preact.Component<{
}
override render() {
this.editor ||= new TeamEditorState(this.props.team);
- this.editor.readonly = !!this.props.readonly;
+ this.editor.setReadonly(!!this.props.readonly);
this.editor.narrow = this.props.narrow ?? document.body.offsetWidth < 500;
if (this.props.team.format !== this.editor.format) {
this.editor.setFormat(this.props.team.format);
@@ -714,7 +730,10 @@ class TeamTextbox extends preact.Component<{ editor: TeamEditorState, onChange?:
this.heightTester.value = fullLine && !newValue.endsWith('\n') ? newValue + '\n' : newValue;
return this.heightTester.scrollHeight;
}
- input = () => this.updateText();
+ input = () => {
+ this.updateText();
+ this.save();
+ };
keyUp = () => this.updateText(true);
contextMenu = (ev: MouseEvent) => {
if (!ev.shiftKey) {
@@ -786,13 +805,15 @@ class TeamTextbox extends preact.Component<{ editor: TeamEditorState, onChange?:
if (ev.keyCode === 13 && ev.shiftKey) return;
if (ev.altKey || ev.metaKey) return;
if (!this.innerFocus) {
- if (
+ if (this.maybeReplaceLine()) {
+ // do nothing else
+ } else if (
this.textbox.selectionStart === this.textbox.value.length &&
(this.textbox.value.endsWith('\n\n') || !this.textbox.value)
) {
this.addPokemon();
- } else {
- this.openInnerFocus();
+ } else if (!this.openInnerFocus()) {
+ break;
}
ev.stopImmediatePropagation();
ev.preventDefault();
@@ -820,6 +841,24 @@ class TeamTextbox extends preact.Component<{ editor: TeamEditorState, onChange?:
}
}
};
+ maybeReplaceLine = () => {
+ if (this.textbox.selectionStart !== this.textbox.selectionEnd) return;
+ const current = this.textbox.selectionEnd;
+ const lineStart = this.textbox.value.lastIndexOf('\n', current) + 1;
+ const value = this.textbox.value.slice(lineStart, current);
+
+ const pokepaste = /^https?:\/\/pokepast.es\/([a-z0-9]+)(?:\/.*)?$/.exec(value)?.[1];
+ if (pokepaste) {
+ Net(`https://pokepast.es/${pokepaste}/json`).get().then(json => {
+ const paste = JSON.parse(json);
+ // make sure it's still there:
+ const valueIndex = this.textbox.value.indexOf(value);
+ this.replace(paste.paste.replace(/\r\n/g, '\n'), valueIndex, valueIndex + value.length);
+ });
+ return true;
+ }
+ return false;
+ };
getInnerFocusValue() {
if (!this.innerFocus) return '';
return this.textbox.value.slice(this.innerFocus.range[0], this.innerFocus.range[1]);
@@ -840,6 +879,7 @@ class TeamTextbox extends preact.Component<{ editor: TeamEditorState, onChange?:
this.clearInnerFocus();
if (this.setDirty) {
this.updateText();
+ this.save();
} else {
this.forceUpdate();
}
@@ -986,7 +1026,6 @@ class TeamTextbox extends preact.Component<{ editor: TeamEditorState, onChange?:
}
textbox.style.height = `${bottomY + 100}px`;
- this.save();
}
this.forceUpdate();
};
@@ -1180,6 +1219,7 @@ class TeamTextbox extends preact.Component<{ editor: TeamEditorState, onChange?:
// for future updates
if (!this.setInfo[index]) {
this.updateText();
+ this.save();
} else {
if (this.setInfo[index + 1]) {
this.setInfo[index + 1].index = start + newText.length;
@@ -1352,7 +1392,7 @@ class TeamTextbox extends preact.Component<{ editor: TeamEditorState, onChange?:
;
}
+ renderMoveButton(props: {
+ name: string,
+ cmd: string, type: Dex.TypeName, tooltip: string, moveData: { pp?: number, maxpp?: number, disabled?: boolean },
+ } | null) {
+ if (!props) {
+ return ;
+ }
+ const pp = props.moveData.maxpp ? `${props.moveData.pp!}/${props.moveData.maxpp}` : '\u2014';
+ return
+ {props.name}
+ {props.type} {pp}
+ ;
+ }
+ renderPokemonButton(props: {
+ pokemon: Pokemon | ServerPokemon | null, cmd: string, noHPBar?: boolean, disabled?: boolean | 'fade', tooltip: string,
+ }) {
+ const pokemon = props.pokemon;
+ if (!pokemon) {
+ return
+ (empty slot)
+ ;
+ }
+
+ let hpColorClass;
+ switch (BattleScene.getHPColor(pokemon)) {
+ case 'y': hpColorClass = 'hpbar hpbar-yellow'; break;
+ case 'r': hpColorClass = 'hpbar hpbar-red'; break;
+ default: hpColorClass = 'hpbar'; break;
+ }
+
+ return
+ {PSIcon({ pokemon })}
+ {pokemon.name}
+ {
+ !props.noHPBar && !pokemon.fainted &&
+
+
+
+ }
+ {!props.noHPBar && pokemon.status && }
+ ;
+ }
renderMoveMenu(choices: BattleChoiceBuilder) {
const moveRequest = choices.currentMoveRequest()!;
@@ -521,9 +523,13 @@ class BattlePanel extends PSRoomPanel {
}
const gmaxTooltip = maxMoveData.id.startsWith('gmax') ? `|${maxMoveData.id}` : ``;
const tooltip = `maxmove|${moveData.name}|${pokemonIndex}${gmaxTooltip}`;
- return
- {maxMoveData.name}
- ;
+ return this.renderMoveButton({
+ name: maxMoveData.name,
+ cmd: `/move ${i + 1} max`,
+ type: moveType,
+ tooltip,
+ moveData,
+ });
});
}
@@ -534,15 +540,19 @@ class BattlePanel extends PSRoomPanel {
return active.moves.map((moveData, i) => {
const zMoveData = active.zMoves![i];
if (!zMoveData) {
- return ;
+ return this.renderMoveButton(null);
}
const specialMove = dex.moves.get(zMoveData.name);
const move = specialMove.exists ? specialMove : dex.moves.get(moveData.name);
const moveType = tooltips.getMoveType(move, valueTracker)[0];
const tooltip = `zmove|${moveData.name}|${pokemonIndex}`;
- return
- {zMoveData.name}
- ;
+ return this.renderMoveButton({
+ name: zMoveData.name,
+ cmd: `/move ${i + 1} zmove`,
+ type: moveType,
+ tooltip,
+ moveData: { pp: 1, maxpp: 1 },
+ });
});
}
@@ -551,9 +561,13 @@ class BattlePanel extends PSRoomPanel {
const move = dex.moves.get(moveData.name);
const moveType = tooltips.getMoveType(move, valueTracker)[0];
const tooltip = `move|${moveData.name}|${pokemonIndex}`;
- return
- {move.name}
- ;
+ return this.renderMoveButton({
+ name: move.name,
+ cmd: `/move ${i + 1}${special}`,
+ type: moveType,
+ tooltip,
+ moveData,
+ });
});
}
renderMoveTargetControls(request: BattleMoveRequest, choices: BattleChoiceBuilder) {
@@ -577,10 +591,12 @@ class BattlePanel extends PSRoomPanel {
}
if (pokemon?.fainted) pokemon = null;
- return ;
+ return this.renderPokemonButton({
+ pokemon,
+ cmd: disabled ? `` : `/${moveChoice} +${i + 1}`,
+ disabled: disabled && 'fade',
+ tooltip: `activepokemon|1|${i}`,
+ });
}).reverse(),
,
battle.nearSide.active.map((pokemon, i) => {
@@ -593,10 +609,12 @@ class BattlePanel extends PSRoomPanel {
if (moveTarget !== 'adjacentAllyOrSelf' && userSlot === i) disabled = true;
if (pokemon?.fainted) pokemon = null;
- return ;
+ return this.renderPokemonButton({
+ pokemon,
+ cmd: disabled ? `` : `/${moveChoice} -${i + 1}`,
+ disabled: disabled && 'fade',
+ tooltip: `activepokemon|0|${i}`,
+ });
}),
];
}
@@ -616,18 +634,24 @@ class BattlePanel extends PSRoomPanel {
}
{request.side.pokemon.map((serverPokemon, i) => {
const cantSwitch = trapped || i < numActive || choices.alreadySwitchingIn.includes(i + 1) || serverPokemon.fainted;
- return ;
+ return this.renderPokemonButton({
+ pokemon: serverPokemon,
+ cmd: `/switch ${i + 1}`,
+ disabled: cantSwitch,
+ tooltip: `switchpokemon|${i}`,
+ });
})}
;
}
- renderTeamControls(request: | BattleTeamRequest, choices: BattleChoiceBuilder) {
+ renderTeamPreviewChooser(request: | BattleTeamRequest, choices: BattleChoiceBuilder) {
return request.side.pokemon.map((serverPokemon, i) => {
const cantSwitch = choices.alreadySwitchingIn.includes(i + 1);
- return ;
+ return this.renderPokemonButton({
+ pokemon: serverPokemon,
+ cmd: `/switch ${i + 1}`,
+ disabled: cantSwitch && 'fade',
+ tooltip: `switchpokemon|${i}`,
+ });
});
}
renderTeamList() {
@@ -637,9 +661,12 @@ class BattlePanel extends PSRoomPanel {
Team
;
@@ -647,9 +674,12 @@ class BattlePanel extends PSRoomPanel {
renderChosenTeam(request: BattleTeamRequest, choices: BattleChoiceBuilder) {
return choices.alreadySwitchingIn.map(slot => {
const serverPokemon = request.side.pokemon[slot - 1];
- return ;
+ return this.renderPokemonButton({
+ pokemon: serverPokemon,
+ cmd: `/switch ${slot}`,
+ disabled: true,
+ tooltip: `switchpokemon|${slot - 1}`,
+ });
});
}
renderOldChoices(request: BattleRequest, choices: BattleChoiceBuilder) {
@@ -821,7 +851,7 @@ class BattlePanel extends PSRoomPanel {
Choose {choices.alreadySwitchingIn.length <= 0 ? `lead` : `slot ${choices.alreadySwitchingIn.length + 1}`}
diff --git a/play.pokemonshowdown.com/src/panel-chat.tsx b/play.pokemonshowdown.com/src/panel-chat.tsx
index 99cac3f6f..9a81362a2 100644
--- a/play.pokemonshowdown.com/src/panel-chat.tsx
+++ b/play.pokemonshowdown.com/src/panel-chat.tsx
@@ -1221,7 +1221,7 @@ export class ChatLog extends preact.Component<{
if (controlsElem && controlsElem.className !== 'controls') controlsElem = undefined;
if (!jsx) {
if (!controlsElem) return;
- preact.render(null, elem, controlsElem);
+ elem.removeChild(controlsElem);
this.updateScroll();
return;
}
@@ -1230,7 +1230,9 @@ export class ChatLog extends preact.Component<{
controlsElem.className = 'controls';
elem.appendChild(controlsElem);
}
- preact.render({jsx}
, elem, controlsElem);
+ // for some reason, the replaceNode feature isn't working?
+ if (controlsElem.children[0]) controlsElem.removeChild(controlsElem.children[0]);
+ preact.render({jsx}
, controlsElem);
this.updateScroll();
}
updateScroll() {
diff --git a/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx b/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx
index 97c5bc1d6..76f76bb56 100644
--- a/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx
+++ b/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx
@@ -16,7 +16,6 @@ class TeamRoom extends PSRoom {
/** Doesn't _literally_ always exist, but does in basically all code
* and constantly checking for its existence is legitimately annoying... */
team!: Team;
- uploaded = false;
override clientCommands = this.parseClientCommands({
'validate'(target) {
if (this.team.format.length <= 4) {
@@ -32,7 +31,6 @@ class TeamRoom extends PSRoom {
this.team = team!;
this.title = `[Team] ${this.team?.name || 'Error'}`;
if (team) this.setFormat(team.format);
- this.uploaded = !!team?.uploaded;
this.load();
}
setFormat(format: string) {
@@ -60,7 +58,7 @@ class TeamRoom extends PSRoom {
buf.push(exported);
PS.teams.uploading = team;
PS.send(`|/teams ${cmd} ${buf.join(', ')}`);
- this.uploaded = true;
+ team.uploadedPackedTeam = exported;
this.update(null);
}
save() {
@@ -114,11 +112,10 @@ class TeamPanel extends PSRoomPanel {
uploadTeam = (ev: Event) => {
const room = this.props.room;
- room.upload(PS.prefs.uploadprivacy);
+ room.upload(room.team.uploaded ? !!room.team.uploaded.private : PS.prefs.uploadprivacy);
};
changePrivacyPref = (ev: Event) => {
- this.props.room.uploaded = false;
PS.prefs.uploadprivacy = !(ev.currentTarget as HTMLInputElement).checked;
PS.prefs.save();
this.forceUpdate();
@@ -136,7 +133,6 @@ class TeamPanel extends PSRoomPanel {
};
save = () => {
this.props.room.save();
- this.props.room.uploaded = false;
this.forceUpdate();
};
@@ -156,18 +152,20 @@ class TeamPanel extends PSRoomPanel {
const info = TeamPanel.formatResources[team.format];
const formatName = BattleLog.formatName(team.format);
+ const unsaved = team.uploaded ? team.uploadedPackedTeam !== team.packedTeam : false;
return
Teams
{}
- {team.uploaded?.private ? (
-
- Account
-
- ) : team.uploaded ? (
-
- Account (public)
-
+ {team.uploaded ? (
+ <>
+
+ Account {team.uploaded.private ? '' : "(public)"}
+
+ {unsaved &&
+ Upload changes
+ }
+ >
) : team.teamid ? (
Disconnected (wrong account?)
@@ -191,31 +189,36 @@ class TeamPanel extends PSRoomPanel {
onInput={this.handleRename} onChange={this.handleRename} onKeyUp={this.handleRename}
/>
-
+
{!!(team.packedTeam && team.format.length > 4) &&
Validate
}
- {team.uploaded?.private === null &&
- Share URL: {}
-
-
}
- {!!(team.packedTeam || team.uploaded) &&
-
+ {!!(team.packedTeam || team.uploaded) &&
+ {team.uploadedPackedTeam && !team.uploaded ? <>
+ Uploading...
+ > : team.uploaded ? <>
+ Share URL: {}
Public
-
- {room.uploaded ? (
-
- Saved to your account
-
- ) : (
+ type="text" class="textbox" readOnly size={45}
+ value={`https://psim.us/t/${team.uploaded.teamid}${team.uploaded.private ? '-' + team.uploaded.private : ''}`}
+ /> {}
+ {unsaved &&
+
+ Upload changes
+
+
}
+ > : !team.teamid ? <>
+
+ Public
+
- Save to my account {}
- ({PS.prefs.uploadprivacy ? 'use on other devices' : 'share and make searchable'})
+ Upload for shareable URL {}
+ {PS.prefs.uploadprivacy ? '' : '(and make searchable)'}
- )}
+ > : null}
}
{!!(info && (info.resources.length || info.url)) && (
@@ -252,6 +255,7 @@ class TeamStoragePanel extends PSRoomPanel {
PS.mainmenu.send(`/teams delete ${team.uploaded.teamid}`);
team.uploaded = undefined;
team.teamid = undefined;
+ team.uploadedPackedTeam = undefined;
PS.teams.save();
(room.getParent() as TeamRoom).update(null);
} else if (storage === 'public' && team.uploaded?.private) {
diff --git a/play.pokemonshowdown.com/src/panel-teambuilder.tsx b/play.pokemonshowdown.com/src/panel-teambuilder.tsx
index 2c29837f8..0a918232c 100644
--- a/play.pokemonshowdown.com/src/panel-teambuilder.tsx
+++ b/play.pokemonshowdown.com/src/panel-teambuilder.tsx
@@ -470,9 +470,9 @@ class TeambuilderPanel extends PSRoomPanel {
{teams.map(team => team ? (
{}
-
+ {!team.uploaded &&
Delete
- {}
+ } {}
{team.uploaded?.private ? (
) : team.uploaded ? (
diff --git a/play.pokemonshowdown.com/style/client2.css b/play.pokemonshowdown.com/style/client2.css
index dc62a63ef..2e17a6fc3 100644
--- a/play.pokemonshowdown.com/style/client2.css
+++ b/play.pokemonshowdown.com/style/client2.css
@@ -296,7 +296,7 @@ li::marker {
content: "";
display: block;
height: 6px;
- margin: -1px 0 -1px 0;
+ margin: -1px 0 0 0;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.2);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.2);
box-shadow: 0 1px 2px rgba(0,0,0,.2);
@@ -1993,6 +1993,7 @@ pre.textbox.textbox-empty[placeholder]:before {
.switchmenu button.disabled,
.switchmenu button:disabled,
+.movebutton.disabled,
.movebutton:disabled {
cursor: default;
background: #F3F3F3 !important;
@@ -2080,6 +2081,7 @@ pre.textbox.textbox-empty[placeholder]:before {
text-align: center;
/* individual pokemon in a team have margin-right -4px so this compensates */
margin: 0 -1px 0 -5px;
+ white-space: normal;
}
.team small span {
margin-right: -4px;
diff --git a/play.pokemonshowdown.com/style/teambuilder.css b/play.pokemonshowdown.com/style/teambuilder.css
index 8256c6398..37a48940f 100644
--- a/play.pokemonshowdown.com/style/teambuilder.css
+++ b/play.pokemonshowdown.com/style/teambuilder.css
@@ -536,7 +536,7 @@ you can't delete it by pressing Backspace */
border: 0;
border-spacing: 0;
table-layout: fixed;
- margin: 4px 0;
+ margin: 1px 0;
width: 100%;
}
.set-button td {
@@ -570,10 +570,11 @@ you can't delete it by pressing Backspace */
.set-button .set-nickname {
position: absolute;
- height: 35px;
+ height: 32px;
width: 120px;
top: 5px;
left: -5px;
+ padding: 0 3px;
}
.tiny-layout .set-button .set-nickname {
width: 90px;
@@ -661,8 +662,9 @@ you can't delete it by pressing Backspace */
}
.team-focus-editor .tabbar {
/* note to self: make this scrollable later */
- /* overflow: auto; */
+ overflow: auto;
white-space: nowrap;
+ min-height: 59px;
}
.tabbar .button.picontab {
width: 80px;