diff --git a/play.pokemonshowdown.com/src/battle-log.ts b/play.pokemonshowdown.com/src/battle-log.ts index 5398dd3ac..6360a7327 100644 --- a/play.pokemonshowdown.com/src/battle-log.ts +++ b/play.pokemonshowdown.com/src/battle-log.ts @@ -159,7 +159,12 @@ export class BattleLog { let rank = name.charAt(0); if (battle?.ignoreSpects && ' +'.includes(rank)) return; if (battle?.ignoreOpponent) { - if ('\u2605\u2606'.includes(rank) && toUserid(name) !== app.user.get('userid')) return; + if ( + '\u2605\u2606'.includes(rank) && + toUserid(name) !== (window.app?.user?.get('userid') || window.PS?.user?.userid) + ) { + return; + } } const ignoreList = window.app?.ignore || window.PS?.prefs?.ignore; if (ignoreList?.[toUserid(name)] && ' +^\u2605\u2606'.includes(rank)) return; diff --git a/play.pokemonshowdown.com/src/battle.ts b/play.pokemonshowdown.com/src/battle.ts index 85d1fe83d..14ae0a344 100644 --- a/play.pokemonshowdown.com/src/battle.ts +++ b/play.pokemonshowdown.com/src/battle.ts @@ -3507,7 +3507,7 @@ export class Battle { return; } else if (args[1].endsWith(' seconds left.')) { let hasIndex = args[1].indexOf(' has '); - let userid = window.app?.user?.get('userid'); + let userid = window.app?.user?.get('userid') || window.PS?.user.userid; if (toID(args[1].slice(0, hasIndex)) === userid) { this.kickingInactive = parseInt(args[1].slice(hasIndex + 5), 10) || true; } diff --git a/play.pokemonshowdown.com/src/client-main.ts b/play.pokemonshowdown.com/src/client-main.ts index a4441ec85..a53bfdf03 100644 --- a/play.pokemonshowdown.com/src/client-main.ts +++ b/play.pokemonshowdown.com/src/client-main.ts @@ -79,6 +79,13 @@ class PSPrefs extends PSStreamModel { hideinterstice: true, }; + /* Battle preferences */ + ignorenicks: boolean | null = null; + ignorespects: boolean | null = null; + ignoreopp: boolean | null = null; + autotimer: boolean | null = null; + rightpanelbattles: boolean | null = null; + /** * Show "User joined" and "User left" messages. serverid:roomid * table. Uses 1 and 0 instead of true/false for JSON packing @@ -895,6 +902,13 @@ export class PSRoom extends PSStreamModel implements RoomOptions { this.send(target); PS.leave(this.id); }, + 'inopener,inparent'(target) { + // do this command in the popup opener + let room = this.getParent(); + if (room && PS.isPopup(room)) room = room.getParent(); + // will crash if the parent doesn't exist, which is fine + room!.send(target); + }, 'maximize'(target) { const roomid = /[^a-z0-9-]/.test(target) ? toID(target) as any as RoomID : target as RoomID; const targetRoom = roomid ? PS.rooms[roomid] : this; @@ -1561,6 +1575,10 @@ export const PS = new class extends PSModel { } getRoom(elem: HTMLElement | EventTarget | null | undefined, skipClickable?: boolean): PSRoom | null { let curElem: HTMLElement | null = elem as HTMLElement; + // might be the close button on the roomtab + if ((curElem as HTMLButtonElement)?.name === 'closeRoom' && (curElem as HTMLButtonElement).value) { + return PS.rooms[(curElem as HTMLButtonElement).value] || null; + } while (curElem) { if (curElem.id.startsWith('room-')) { return PS.rooms[curElem.id.slice(5)] || null; @@ -1941,7 +1959,7 @@ export const PS = new class extends PSModel { options.args = { initialSlash: true }; } } - + if (options.id.startsWith('battle-') && PS.prefs.rightpanelbattles) options.location = 'right'; options.parentRoomid ??= this.getRoom(options.parentElem)?.id; let preexistingRoom = this.rooms[options.id]; if (preexistingRoom && this.isPopup(preexistingRoom)) { diff --git a/play.pokemonshowdown.com/src/panel-battle.tsx b/play.pokemonshowdown.com/src/panel-battle.tsx index 4babdbbdd..5cd40d522 100644 --- a/play.pokemonshowdown.com/src/panel-battle.tsx +++ b/play.pokemonshowdown.com/src/panel-battle.tsx @@ -133,6 +133,7 @@ export class BattleRoom extends ChatRoom { side: BattleRequestSideInfo | null = null; request: BattleRequest | null = null; choices: BattleChoiceBuilder | null = null; + autoTimerActivated: boolean | null = null; } class BattleDiv extends preact.Component<{ room: BattleRoom }> { @@ -196,6 +197,64 @@ function PokemonButton(props: { ; } +class TimerButton extends preact.Component<{ room: BattleRoom }> { + timerInterval: number | null = null; + override componentWillUnmount() { + if (this.timerInterval) { + clearInterval(this.timerInterval); + this.timerInterval = null; + } + } + secondsToTime(seconds: number | true) { + if (seconds === true) return '-:--'; + const minutes = Math.floor(seconds / 60); + seconds -= minutes * 60; + return `${minutes}:${(seconds < 10 ? '0' : '')}${seconds}`; + } + render() { + let time = 'Timer'; + const room = this.props.room; + if (!this.timerInterval && room.battle.kickingInactive) { + this.timerInterval = setInterval(() => { + if (typeof room.battle.kickingInactive === 'number' && room.battle.kickingInactive > 1) { + room.battle.kickingInactive--; + if (room.battle.graceTimeLeft) room.battle.graceTimeLeft--; + else if (room.battle.totalTimeLeft) room.battle.totalTimeLeft--; + } + this.forceUpdate(); + }, 1000); + } else if (this.timerInterval && !room.battle.kickingInactive) { + clearInterval(this.timerInterval); + this.timerInterval = null; + } + + let timerTicking = (room.battle.kickingInactive && + room.request && room.request.requestType !== "wait" && (room.choices && !room.choices.isDone())) ? + ' timerbutton-on' : ''; + + if (room.battle.kickingInactive) { + const secondsLeft = room.battle.kickingInactive; + time = this.secondsToTime(secondsLeft); + if (secondsLeft !== true) { + if (secondsLeft <= 10 && timerTicking) { + timerTicking = ' timerbutton-critical'; + } + + if (room.battle.totalTimeLeft) { + const totalTime = this.secondsToTime(room.battle.totalTimeLeft); + time += ` | ${totalTime} total`; + } + } + } + + return ; + } +}; + class BattlePanel extends PSRoomPanel { static readonly id = 'battle'; static readonly routes = ['battle-*']; @@ -316,6 +375,11 @@ class BattlePanel extends PSRoomPanel { return; } + if (PS.prefs.autotimer && !room.battle.kickingInactive && !room.autoTimerActivated) { + this.send('/timer on'); + room.autoTimerActivated = true; + } + BattleChoiceBuilder.fixRequest(request, room.battle); if (request.side) { @@ -569,9 +633,6 @@ class BattlePanel extends PSRoomPanel { if (choices.isDone()) { return
- {this.renderOldChoices(request, choices)}
@@ -599,9 +660,6 @@ class BattlePanel extends PSRoomPanel { const moveName = choices.getChosenMove(choices.current, choices.index()).name; return
- {this.renderOldChoices(request, choices)} {pokemon.name} should use {moveName} at where? {}
@@ -617,9 +675,6 @@ class BattlePanel extends PSRoomPanel { return
- {this.renderOldChoices(request, choices)} What will {pokemon.name} do?
@@ -674,9 +729,6 @@ class BattlePanel extends PSRoomPanel { const pokemon = request.side.pokemon[choices.index()]; return
- {this.renderOldChoices(request, choices)} What will {pokemon.name} do?
@@ -690,9 +742,6 @@ class BattlePanel extends PSRoomPanel { } case 'team': { return
- {choices.alreadySwitchingIn.length > 0 ? ( [, " What about the rest of your team? "] @@ -765,7 +814,6 @@ class BattlePanel extends PSRoomPanel {

)}
; - } handleDownloadReplay = (e: MouseEvent) => { @@ -787,9 +835,14 @@ class BattlePanel extends PSRoomPanel { override render() { const room = this.props.room; this.updateLayout(); + const id = `room-${room.id}`; + const hardcoreStyle = room.battle?.hardcoreMode ? : null; if (room.width < 700) { return + {hardcoreStyle} { + + {(room.battle && !room.battle.ended && room.request && room.battle.mySide.id === PS.user.userid) && + }
; } return + {hardcoreStyle} { +
diff --git a/play.pokemonshowdown.com/src/panel-mainmenu.tsx b/play.pokemonshowdown.com/src/panel-mainmenu.tsx index 7e3009cf4..47f6843bc 100644 --- a/play.pokemonshowdown.com/src/panel-mainmenu.tsx +++ b/play.pokemonshowdown.com/src/panel-mainmenu.tsx @@ -503,7 +503,7 @@ class MainMenuPanel extends PSRoomPanel {

- {} - {!battleRoom.battle.rated && } {} - -

- +

Forfeiting makes you lose the battle. Are you sure?

+

+ {} + {} + {!battleRoom.battle.rated && } {} + +

; } } @@ -1400,6 +1382,181 @@ class LeaveRoomPanel extends PSRoomPanel {
; } } +class BattleOptionsPanel extends PSRoomPanel { + static readonly id = 'battleoptions'; + static readonly routes = ['battleoptions']; + static readonly location = 'semimodal-popup'; + static readonly noURL = true; + + handleHardcoreMode = (ev: Event) => { + const mode = (ev.currentTarget as HTMLInputElement).checked; + const room = this.props.room.getParent() as BattleRoom; + if (!room?.battle) return this.close(); + room.battle.setHardcoreMode(mode); + if (mode) { + room.add(`||Hardcore mode ON: Information not available in-game is now hidden.`); + } else { + room.add(`||Hardcore mode OFF: Information not available in-game is now shown.`); + } + room.update(null); + }; + handleIgnoreSpectators = (ev: Event | boolean) => { + const value = typeof ev === "object" ? + (ev.currentTarget as HTMLInputElement).checked : + ev; + const room = this.props.room.getParent() as BattleRoom; + if (!room?.battle) return this.close(); + room.battle.ignoreSpects = value; + room.add(`||Spectators ${room.battle.ignoreSpects ? '' : 'no longer '}ignored.`); + const chats = document.querySelectorAll('.battle-log .chat'); + const displaySetting = room.battle.ignoreSpects ? 'none' : ''; + for (const chat of chats) { + const small = chat.querySelector('small'); + if (!small) continue; + const text = small.innerText; + const isPlayerChat = text.includes('\u2606') || text.includes('\u2605'); + if (!isPlayerChat) { + chat.style.display = displaySetting; + } + } + room.battle.scene.log.updateScroll(); + }; + handleIgnoreOpponent = (ev: Event | boolean) => { + const value = typeof ev === "object" ? + (ev.currentTarget as HTMLInputElement).checked : + ev; + const room = this.props.room.getParent() as BattleRoom; + if (!room?.battle) return this.close(); + room.battle.ignoreOpponent = value; + room.battle.resetToCurrentTurn(); + }; + handleIgnoreNicks = (ev: Event | boolean) => { + const value = typeof ev === "object" ? + (ev.currentTarget as HTMLInputElement).checked : + ev; + const room = this.props.room.getParent() as BattleRoom; + if (!room?.battle) return this.close(); + room.battle.ignoreNicks = value; + room.battle.resetToCurrentTurn(); + }; + handleAllSettings = (ev: Event) => { + const setting = (ev.currentTarget as HTMLInputElement).name; + const value = (ev.currentTarget as HTMLInputElement).checked; + const room = this.props.room.getParent() as BattleRoom; + if (!room?.battle) return this.close(); + switch (setting) { + case 'autotimer': { + PS.prefs.set('autotimer', value); + if (value) { + room.send('/timer on'); + } + break; + } + case 'ignoreopp': { + PS.prefs.set('ignoreopp', value); + this.handleIgnoreOpponent(value); + break; + } + case 'ignorespects': { + PS.prefs.set('ignorespects', value); + this.handleIgnoreSpectators(value); + break; + } + case 'ignorenicks': { + PS.prefs.set('ignorenicks', value); + this.handleIgnoreNicks(value); + break; + } + case 'rightpanel': { + PS.prefs.set('rightpanelbattles', value); + } + } + }; + + override render() { + const room = this.props.room; + const battleRoom = this.props.room.getParent() as BattleRoom; + + return
+

In this battle

+

+ +

+

+ +

+

+ +

+

+ +

+

All battles

+

+ +

+

+ +

+

+ +

+

+ +

+

+ +

+

+
+
; + } +} class PopupRoom extends PSRoom { returnValue: unknown = this.args?.cancelValue; @@ -1487,6 +1644,24 @@ class RoomTabListPanel extends PSRoomPanel {
; } } +class BattleTimerPanel extends PSRoomPanel { + static readonly id = 'battletimer'; + static readonly routes = ['battletimer']; + static readonly location = 'semimodal-popup'; + static readonly noURL = true; + + override render() { + const room = this.props.room.getParent() as BattleRoom; + return
+ {room.battle.kickingInactive ? ( + + ) : ( + + )} +
+
; + } +} PS.addRoomType( UserPanel, @@ -1504,5 +1679,7 @@ PS.addRoomType( LeaveRoomPanel, ChatFormattingPanel, PopupPanel, - RoomTabListPanel + RoomTabListPanel, + BattleOptionsPanel, + BattleTimerPanel );