diff --git a/README.md b/README.md index ac02c2e5c..2c54e509e 100644 --- a/README.md +++ b/README.md @@ -45,18 +45,15 @@ New client Development is proceeding on the client rewrite! The live version is available at https://play.pokemonshowdown.com/beta -You can contribute to it yourself using the same process as before, just -use `testclient-new.html` rather than `testclient-old.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. +(on other OSes) to build after each change. + +(This will do a fast build with no checks. For tests, linting, and +typechecking, you'll want to do `npm test` separately.) You can make and test client changes simply by building after each change, -and opening `play.pokemonshowdown.com/testclient-old.html`. This will allow you +and opening `play.pokemonshowdown.com/testclient-new.html`. This will allow you to test changes to the client without setting up your own login server. ### Test keys @@ -65,9 +62,8 @@ For security reasons, browsers [don't let other websites control PS][5], so they can't screw with your account, but it does make it harder to log in on the test client. -The default hack makes you copy/paste the data instead, but if you're -refreshing a lot, just add a `config/testclient-key.js` file, with the -contents: +If you need a logged-in account, add a `config/testclient-key.js` file, with +the contents: const POKEMON_SHOWDOWN_TESTCLIENT_KEY = 'sid'; @@ -78,21 +74,19 @@ grab it from: Make sure to put it in `config/` and not `play.pokemonshowdown.com/config/`. -(This is the only supported method of logging in on the beta testclient.) - [5]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS ### Other servers You can connect to an arbitrary server by navigating to -`testclient-old.html?~~host:port`. For example, to connect to a server running -locally on port 8000, you can navigate to `testclient-old.html?~~localhost:8000`. +`testclient-new.html?~~host:port`. For example, to connect to a server running +locally on port 8000, you can navigate to `testclient-new.html?~~localhost:8000`. **NOTE**: Certain browsers will convert `'?'` to `'%3F'` when reading files off of the local filesystem. As a workaround, try using a different browser or serving the files locally first (ie. run `npx http-server` from the directory this README is in, then navigate in your browser to -`http://localhost:8080/testclient-old.html?~~localhost:8000`). +`http://localhost:8080/testclient-new.html?~~localhost:8000`). ### Limitations diff --git a/play.pokemonshowdown.com/src/battle-team-editor.tsx b/play.pokemonshowdown.com/src/battle-team-editor.tsx index 516d9f061..5ac160e14 100644 --- a/play.pokemonshowdown.com/src/battle-team-editor.tsx +++ b/play.pokemonshowdown.com/src/battle-team-editor.tsx @@ -2501,7 +2501,7 @@ class TeamWizard extends preact.Component<{ Undo delete

: null; - return
+ return
{editor.sets.map((set, i) => [ pasteControls(i), this.renderSet(set, i), @@ -3156,7 +3156,7 @@ class TeamEditorForm extends TeamWizard { } cur(type: InnerFocusType, setIndex: number, typeIndex = -1) { const focus = this.props.editor.innerFocus; - return this.props.editor.readonly || ( + return ( focus?.type === type && focus.setIndex === setIndex && focus.typeIndex === typeIndex ) ? ' cur' : ''; } @@ -3233,12 +3233,12 @@ class TeamEditorForm extends TeamWizard { "Copy/Move" } {} - {} + } {} {!(TeamEditorState.clipboard || editor.readonly) && + + ; + } /** Converts DV/IV in a textbox to the value in set. */ dvToIv(dvOrIvString?: string): number | null { const dvOrIv = Number(dvOrIvString); @@ -3941,7 +3979,14 @@ class StatForm extends preact.Component<{ const useIVs = editor.gen > 2; // label column - const statNames = { + const statNames = editor.narrow ? { + hp: 'HP', + atk: 'Atk', + def: 'Def', + spa: 'SpA', + spd: 'SpD', + spe: 'Spe', + } : { hp: 'HP', atk: 'Attack', def: 'Defense', @@ -3990,17 +4035,18 @@ class StatForm extends preact.Component<{ {this.renderStatbar(stat, statID)} + />{this.renderNatureButtons(statID)} {!editor.isChampions && } {stat} @@ -4022,7 +4068,7 @@ class StatForm extends preact.Component<{ ))}

} - {editor.gen >= 3 &&

+ {editor.gen >= 3 && !editor.narrow &&

Protip: You can also set natures by typing + and - in the EV box.

} {editor.gen >= 3 && this.renderStatOptimizer()} diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index 169c55b40..92bc27873 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -23,6 +23,9 @@ import type preact from '../js/lib/preact'; declare const BattleTextAFD: any; declare const BattleTextNotAFD: any; +export const VERTICAL_HEADER_WIDTH = 240; +export const NARROW_MODE_HEADER_WIDTH = 280; + /********************************************************************** * Config *********************************************************************/ @@ -1966,11 +1969,18 @@ export const PS = new class extends PSModel { /** * * 0 = only one panel visible * * null = vertical nav layout - * n.b. PS will only update if the left room width changes. Resizes - * that don't change the left room width will not trigger an update. + * n.b. Resizes only trigger a re-render if the panel layout or a + * width-dependent layout breakpoint changes. */ leftPanelWidth: number | null = 0; mainmenu: MainMenuRoom = null!; + layoutViewportWidth = 0; + + roomWidthBreakpointPassed(oldWidth: number, newWidth: number) { + return (oldWidth < 550) !== (newWidth < 550) || // chat-room userlists, teambuilder + (oldWidth < 620) !== (newWidth < 620) || // main menu and teambuilder tiny-layout class + (oldWidth <= 700) !== (newWidth <= 700); // battle tiny-layout + } /** * The drag-and-drop API is incredibly dumb and doesn't let us know @@ -2101,27 +2111,37 @@ export const PS = new class extends PSModel { /** @returns changed */ updateLayout(): boolean { const leftPanelWidth = this.calculateLeftPanelWidth(); + const viewportWidth = document.documentElement.clientWidth; const totalWidth = document.body.offsetWidth; const totalHeight = document.body.offsetHeight; const roomHeight = totalHeight - 56; + let needsUpdate = this.leftPanelWidth !== leftPanelWidth; if (leftPanelWidth === null) { - this.panel.width = totalWidth - 200; - this.panel.height = totalHeight; + const headerWidth = viewportWidth <= 700 ? + NARROW_MODE_HEADER_WIDTH : VERTICAL_HEADER_WIDTH; + const roomWidth = totalWidth + 1 - headerWidth; + needsUpdate ||= this.roomWidthBreakpointPassed(this.panel.width, roomWidth); + this.panel.width = roomWidth; + this.panel.height = totalHeight - 30; } else if (leftPanelWidth) { + const rightPanelWidth = totalWidth + 1 - leftPanelWidth; + needsUpdate ||= this.roomWidthBreakpointPassed(this.leftPanel.width, leftPanelWidth); + needsUpdate ||= this.roomWidthBreakpointPassed(this.rightPanel!.width, rightPanelWidth); this.leftPanel.width = leftPanelWidth; this.leftPanel.height = roomHeight; - this.rightPanel!.width = totalWidth + 1 - leftPanelWidth; + this.rightPanel!.width = rightPanelWidth; this.rightPanel!.height = roomHeight; } else { + needsUpdate ||= this.roomWidthBreakpointPassed(this.panel.width, totalWidth); this.panel.width = totalWidth; this.panel.height = roomHeight; } if (this.leftPanelWidth !== leftPanelWidth) { this.leftPanelWidth = leftPanelWidth; - return true; } - return false; + this.layoutViewportWidth = viewportWidth; + return needsUpdate; } getRoom(elem: HTMLElement | EventTarget | null | undefined, skipClickable?: boolean): PSRoom | null { let curElem: HTMLElement | null = elem as HTMLElement; @@ -2286,6 +2306,9 @@ export const PS = new class extends PSModel { return room === this.rightPanel || room === this.leftPanel || room === this.room; } } + /** + * @see {@link leftPanelWidth} for return value meaning + */ calculateLeftPanelWidth() { const available = document.body.offsetWidth; if (document.documentElement.clientWidth < 800 || this.prefs.onepanel === 'vertical') { @@ -2328,12 +2351,26 @@ export const PS = new class extends PSModel { } createRoom(options: RoomOptions) { options.location ||= this.getRouteLocation(options.id); + options.location = this.normalizeRoomLocation(options.location); options.type ||= this.getRoute(options.id) || ''; const RoomType = this.roomTypes[options.type]; options.noURL ??= RoomType?.noURL; if (RoomType?.title) options.title = RoomType.title; const Model = RoomType ? (RoomType.Model || PSRoom) : PlaceholderRoom; - return new Model(options); + const room = new Model(options); + room.location = this.normalizeRoomLocation(room.location); + return room; + } + normalizeRoomLocation(location: string): PSRoomLocation; + normalizeRoomLocation(location: string | undefined): PSRoomLocation | undefined; + normalizeRoomLocation(location: any): any { + if (!location) return location; + switch (location) { + case 'left': case 'right': case 'popup': case 'mini-window': case 'modal-popup': + return location; + default: + return 'modal-popup'; + } } getRouteInfo(roomid: RoomID) { if (this.routes[roomid]) return this.routes[roomid]; @@ -2353,8 +2390,10 @@ export const PS = new class extends PSModel { } const routeInfo = this.getRouteInfo(roomid); if (!routeInfo) return 'left'; - if (routeInfo.startsWith('*')) return routeInfo.slice(1) as PSRoomLocation; - return PS.roomTypes[routeInfo]!.location || 'left'; + if (routeInfo.startsWith('*')) { + return this.normalizeRoomLocation(routeInfo.slice(1)); + } + return this.normalizeRoomLocation(PS.roomTypes[routeInfo]!.location) || 'left'; } getRoute(roomid: RoomID) { const routeInfo = this.getRouteInfo(roomid); @@ -2671,6 +2710,7 @@ export const PS = new class extends PSModel { return room.location === 'left' || room.location === 'right'; } moveRoom(room: PSRoom, location: PSRoomLocation, background?: boolean, index?: number) { + location = this.normalizeRoomLocation(location); if (room.location === location && index === undefined) { if (background === true) { if (room === this.leftPanel) { diff --git a/play.pokemonshowdown.com/src/panel-battle.tsx b/play.pokemonshowdown.com/src/panel-battle.tsx index a4027c4cc..54666b506 100644 --- a/play.pokemonshowdown.com/src/panel-battle.tsx +++ b/play.pokemonshowdown.com/src/panel-battle.tsx @@ -147,13 +147,15 @@ export class BattleRoom extends ChatRoom { request: BattleRequest | null = null; choices: BattleChoiceBuilder | null = null; autoTimerActivated: boolean | null = null; + requireForfeit = false; /** should be false if we joined right after accepting or challenging a battle, * and true if we refreshed and rejoined a battle. * null = initializing, we don't know yet */ rejoining: boolean | null = null; override interruptClose(explicit?: boolean, elem?: HTMLElement | null) { - if (!this.battle.ended && this.users[PS.user.userid]?.startsWith('☆') && !this.battle.isReplay) { + const battle = this.battle; + if ((battle && !battle.ended && this.side && !battle.isReplay) || this.requireForfeit) { PS.join('forfeitbattle' as RoomID, { parentElem: elem, parentRoomid: this.id }); return `You are still in ${this.title}`; } @@ -404,6 +406,12 @@ class BattlePanel extends PSRoomPanel { override receiveLine(args: Args) { const room = this.props.room; switch (args[0]) { + case 'cantleave': + room.requireForfeit = true; + return; + case 'allowleave': + room.requireForfeit = false; + return; case 'initdone': if (!PS.prefs.spectatefromstart) room.battle.seekTurn(Infinity); return; @@ -1095,7 +1103,7 @@ class BattlePanel extends PSRoomPanel { dangerouslySetInnerHTML={{ __html: `#${id} .battle .turn, #${id} .battle-history { display: none !important; }` }} > : null; - if (room.width < 700) { + if (room.width <= 700) { return {hardcoreStyle} diff --git a/play.pokemonshowdown.com/src/panel-chat.tsx b/play.pokemonshowdown.com/src/panel-chat.tsx index 23483bb25..ef99dfe1e 100644 --- a/play.pokemonshowdown.com/src/panel-chat.tsx +++ b/play.pokemonshowdown.com/src/panel-chat.tsx @@ -1422,7 +1422,7 @@ class ChatPanel extends PSRoomPanel { override render() { const room = this.props.room; - const tinyLayout = room.width < 450; + const tinyLayout = room.width < 550; const challengeOpen = room.challengeMenuOpen || room.challenging || room.challenged; return diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index 58f3af011..e52a5cdcf 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -1082,6 +1082,7 @@ export class TeamForm extends preact.Component<{