From e5c25bbb97eff6616c9a6ee1302b5b7dce03edc1 Mon Sep 17 00:00:00 2001 From: Guangcong Luo Date: Mon, 5 May 2025 09:19:14 +0000 Subject: [PATCH] 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` --- README.md | 15 ++- .../src/battle-choices.ts | 117 ++++++++++++------ .../src/battle-team-editor.tsx | 43 ++++--- .../src/battle-tooltips.ts | 9 +- play.pokemonshowdown.com/src/battle.ts | 2 +- play.pokemonshowdown.com/src/client-main.ts | 9 +- play.pokemonshowdown.com/src/panel-battle.tsx | 22 ++-- play.pokemonshowdown.com/src/panel-chat.tsx | 4 + .../src/panel-mainmenu.tsx | 16 +-- .../src/panel-teambuilder-team.tsx | 19 ++- .../src/panel-teamdropdown.tsx | 38 +++--- play.pokemonshowdown.com/src/panel-topbar.tsx | 8 +- play.pokemonshowdown.com/src/panels.tsx | 65 +++++++++- play.pokemonshowdown.com/style/client2.css | 1 + .../style/teambuilder.css | 28 +++-- 15 files changed, 273 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index 97de6f928..26e6fea4b 100644 --- a/README.md +++ b/README.md @@ -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, diff --git a/play.pokemonshowdown.com/src/battle-choices.ts b/play.pokemonshowdown.com/src/battle-choices.ts index de8211492..68bdef152 100644 --- a/play.pokemonshowdown.com/src/battle-choices.ts +++ b/play.pokemonshowdown.com/src/battle-choices.ts @@ -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( diff --git a/play.pokemonshowdown.com/src/battle-team-editor.tsx b/play.pokemonshowdown.com/src/battle-team-editor.tsx index 4e9f2369e..2e0412bd9 100644 --- a/play.pokemonshowdown.com/src/battle-team-editor.tsx +++ b/play.pokemonshowdown.com/src/battle-team-editor.tsx @@ -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 {} @@ -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 = () => { diff --git a/play.pokemonshowdown.com/src/battle-tooltips.ts b/play.pokemonshowdown.com/src/battle-tooltips.ts index 331226883..c2414a6f6 100644 --- a/play.pokemonshowdown.com/src/battle-tooltips.ts +++ b/play.pokemonshowdown.com/src/battle-tooltips.ts @@ -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 diff --git a/play.pokemonshowdown.com/src/battle.ts b/play.pokemonshowdown.com/src/battle.ts index 38aaebfd5..1b6838c7b 100644 --- a/play.pokemonshowdown.com/src/battle.ts +++ b/play.pokemonshowdown.com/src/battle.ts @@ -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 } = {}; diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index 3d288f928..6ec696f38 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -999,12 +999,14 @@ export class PSRoom extends PSStreamModel 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 implements RoomOptions { } this.add("||All PM windows cleared and closed."); }, + 'senddirect'(target) { + this.sendDirect(target); + }, 'help'(target) { switch (toID(target)) { case 'chal': diff --git a/play.pokemonshowdown.com/src/panel-battle.tsx b/play.pokemonshowdown.com/src/panel-battle.tsx index 6b6650393..63f9c8e58 100644 --- a/play.pokemonshowdown.com/src/panel-battle.tsx +++ b/play.pokemonshowdown.com/src/panel-battle.tsx @@ -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} > - + {pokemon.name} { !props.noHPBar && !pokemon.fainted && @@ -541,7 +541,7 @@ class BattlePanel extends PSRoomPanel { } 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 { } 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 { 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({err.message}); + } 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 { if (choice.ultra) buf.push(Ultra, ` Burst and `); if (choice.tera) buf.push(`Terastallize (`, {active?.canTerastallize || '???'}, `) and `); if (choice.max && active?.canDynamax) buf.push(active?.canGigantamax ? `Gigantamax and ` : `Dynamax and `); - buf.push(`use `, {choices.getChosenMove(choice, i).name}); + buf.push(`use `, {choices.currentMove(choice, i)?.name}); if (choice.targetLoc > 0) { const target = battle.farSide.active[choice.targetLoc - 1]; if (!target) { @@ -686,6 +691,9 @@ class BattlePanel extends PSRoomPanel { buf.push(`${pokemon.name} will switch to `, {target.name}); } else if (choice.choiceType === 'shift') { buf.push(`${pokemon.name} will `, shift, ` to the center`); + } else if (choice.choiceType === 'team') { + const target = request.side.pokemon[choice.targetPokemon - 1]; + buf.push(`You picked `, {target.name}); } buf.push(
); } @@ -718,7 +726,7 @@ class BattlePanel extends PSRoomPanel { 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
{this.renderOldChoices(request, choices)} diff --git a/play.pokemonshowdown.com/src/panel-chat.tsx b/play.pokemonshowdown.com/src/panel-chat.tsx index 482fcdf74..cbb38bdf3 100644 --- a/play.pokemonshowdown.com/src/panel-chat.tsx +++ b/play.pokemonshowdown.com/src/panel-chat.tsx @@ -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); diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index 16d344f8e..ec604c3a2 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -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 ; diff --git a/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx b/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx index e2f0afdcb..227eeeaba 100644 --- a/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx +++ b/play.pokemonshowdown.com/src/panel-teambuilder-team.tsx @@ -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 { static readonly id = 'team'; static readonly routes = ['team-*']; @@ -50,7 +51,7 @@ class TeamPanel extends PSRoomPanel { 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 { } } + static formatResources = {} as Record; + + static getFormatResources(format: string): Promise { + 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; diff --git a/play.pokemonshowdown.com/src/panel-teamdropdown.tsx b/play.pokemonshowdown.com/src/panel-teamdropdown.tsx index 77157d820..74f0c1d4e 100644 --- a/play.pokemonshowdown.com/src/panel-teamdropdown.tsx +++ b/play.pokemonshowdown.com/src/panel-teamdropdown.tsx @@ -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; - - static getFormatResources(format: string): Promise { - 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 = (empty team); } else { icons = PSTeambuilder.packedTeamNames(team.packedTeam).map(species => + // can't use PSIcon, weird interaction with iconCache ); } @@ -648,9 +633,14 @@ export function TeamBox(props: { team: Team | null, noLink?: boolean, button?: b {contents} ; } - return
+ if (props.noLink) { + return
+ {contents} +
; + } + return {contents} -
; + ; } /** diff --git a/play.pokemonshowdown.com/src/panel-topbar.tsx b/play.pokemonshowdown.com/src/panel-topbar.tsx index 14a6b4f96..8acb475a6 100644 --- a/play.pokemonshowdown.com/src/panel-topbar.tsx +++ b/play.pokemonshowdown.com/src/panel-topbar.tsx @@ -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) { diff --git a/play.pokemonshowdown.com/src/panels.tsx b/play.pokemonshowdown.com/src/panels.tsx index 6d7c4438b..2060bed8f 100644 --- a/play.pokemonshowdown.com/src/panels.tsx +++ b/play.pokemonshowdown.com/src/panels.tsx @@ -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('tr.hidden')) { + row.style.display = 'table-row'; + } + for (const row of table.querySelectorAll('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 {
; } } + +export function PSIcon( + props: { pokemon: string | Pokemon | ServerPokemon | Dex.PokemonSet | null } | + { item: string } | { type: string, b?: boolean } | { category: string } +) { + if ('pokemon' in props) { + return ; + } + if ('item' in props) { + return ; + } + if ('type' in props) { + let type = Dex.types.get(props.type).name; + if (!type) type = '???'; + let sanitizedType = type.replace(/\?/g, '%3f'); + return {type}; + } + 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 {sanitizedCategory}; + } + return null!; +} diff --git a/play.pokemonshowdown.com/style/client2.css b/play.pokemonshowdown.com/style/client2.css index a58734cdd..fe965988b 100644 --- a/play.pokemonshowdown.com/style/client2.css +++ b/play.pokemonshowdown.com/style/client2.css @@ -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; diff --git a/play.pokemonshowdown.com/style/teambuilder.css b/play.pokemonshowdown.com/style/teambuilder.css index 0fb0d3b34..1b4662ef2 100644 --- a/play.pokemonshowdown.com/style/teambuilder.css +++ b/play.pokemonshowdown.com/style/teambuilder.css @@ -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;