mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-03-21 17:50:29 -05:00
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:
parent
6c80aa25a2
commit
e5c25bbb97
15
README.md
15
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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 } = {};
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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!;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user