diff --git a/.eslintignore b/.eslintignore index 5b37a9993..7a50cdcf0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -27,3 +27,4 @@ node_modules/ /js/panel-teambuilder.js /js/panel-teambuilder-team.js /js/panel-teamdropdown.js +/js/panel-battle.js diff --git a/.gitignore b/.gitignore index fe765e3ad..fd4f62080 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,7 @@ package-lock.json /js/panel-teambuilder.js /js/panel-teambuilder-team.js /js/panel-teamdropdown.js +/js/panel-battle.js /replays/caches/ /replays/replay-config.inc.php diff --git a/preactalpha.template.html b/preactalpha.template.html index 6f578099f..f6a96bd46 100644 --- a/preactalpha.template.html +++ b/preactalpha.template.html @@ -68,6 +68,15 @@ + + + + + + + + + diff --git a/src/battle-log.ts b/src/battle-log.ts index 99be03a5b..f2ba05e0c 100644 --- a/src/battle-log.ts +++ b/src/battle-log.ts @@ -154,6 +154,7 @@ class BattleLog { case 'seed': case 'choice': case ':': case 'timer': case 'J': case 'L': case 'N': case 'spectator': case 'spectatorleave': + case 'initdone': return; default: @@ -190,12 +191,17 @@ class BattleLog { case 'turn': const h2elem = document.createElement('h2'); h2elem.className = 'battle-history'; - let turnMessage = this.battleParser!.parseArgs(args, {}).trim(); - if (!turnMessage.startsWith('==') || !turnMessage.endsWith('==')) { - throw new Error("Turn message must be a heading."); + let turnMessage; + if (this.battleParser) { + turnMessage = this.battleParser.parseArgs(args, {}).trim(); + if (!turnMessage.startsWith('==') || !turnMessage.endsWith('==')) { + throw new Error("Turn message must be a heading."); + } + turnMessage = turnMessage.slice(2, -2).trim(); + this.battleParser.curLineSection = 'break'; + } else { + turnMessage = `Turn ${args[1]}`; } - turnMessage = turnMessage.slice(2, -2).trim(); - this.battleParser!.curLineSection = 'break'; h2elem.innerHTML = BattleLog.escapeHTML(turnMessage); this.addSpacer(); this.addNode(h2elem); diff --git a/src/battle.ts b/src/battle.ts index edb3383cf..54bb27eff 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -3482,7 +3482,11 @@ class Battle { else this.paused = false; this.fastForwardWillScroll = true; } - if (!time) return; + if (!time) { + this.fastForwardOff(); + this.nextActivity(); + return; + } this.scene.animationOff(); this.playbackState = Playback.Seeking; this.fastForward = time; diff --git a/src/client-connection.ts b/src/client-connection.ts index 4a67809a0..23538723d 100644 --- a/src/client-connection.ts +++ b/src/client-connection.ts @@ -11,7 +11,6 @@ class PSConnection { socket: any = null; connected = false; queue = [] as string[]; - hostCheckInterval: number | undefined | null; constructor() { this.connect(); } @@ -26,7 +25,6 @@ class PSConnection { PS.connected = true; for (const msg of this.queue) socket.send(msg); this.queue = []; - this.hostCheckInterval = setTimeout(() => this.doHostCheck(), 500); PS.update(); }; socket.onmessage = (e: MessageEvent) => { @@ -34,10 +32,6 @@ class PSConnection { }; socket.onclose = () => { console.log('\u2705 (DISCONNECTED)'); - if (this.hostCheckInterval !== null) { - clearTimeout(this.hostCheckInterval); - this.hostCheckInterval = null; - } this.connected = false; PS.connected = false; PS.isOffline = true; @@ -48,13 +42,6 @@ class PSConnection { PS.update(); }; } - doHostCheck() { - if (PS.server.host !== $.trim(PS.server.host)) { - this.socket.close(); - } else { - this.hostCheckInterval = setTimeout(() => this.doHostCheck(), 500); - } - } send(msg: string) { if (!this.connected) { this.queue.push(msg); diff --git a/src/client-main.ts b/src/client-main.ts index d47f96db6..431839325 100644 --- a/src/client-main.ts +++ b/src/client-main.ts @@ -648,8 +648,10 @@ const PS = new class extends PSModel { const roomid2 = roomid || 'lobby' as RoomID; let room = PS.rooms[roomid]; console.log('\u2705 ' + (roomid ? '[' + roomid + '] ' : '') + '%c' + msg, "color: #007700"); + let isInit = false; for (const line of msg.split('\n')) { if (line.startsWith('|init|')) { + isInit = true; room = PS.rooms[roomid2]; const type = line.slice(6); if (!room) { @@ -689,7 +691,7 @@ const PS = new class extends PSModel { } if (room) room.receive(line); } - if (room) room.update(null); + if (room) room.update(isInit ? `|initdone` : null); } send(fullMsg: string) { const pipeIndex = fullMsg.indexOf('|'); diff --git a/src/panel-battle.tsx b/src/panel-battle.tsx new file mode 100644 index 000000000..48916a73a --- /dev/null +++ b/src/panel-battle.tsx @@ -0,0 +1,148 @@ +/** + * Battle panel + * + * @author Guangcong Luo + * @license AGPLv3 + */ + +class BattleRoom extends ChatRoom { + readonly classType = 'battle'; + // @ts-ignore assigned in parent constructor + pmTarget: null; + // @ts-ignore assigned in parent constructor + challengeMenuOpen: false; + // @ts-ignore assigned in parent constructor + challengingFormat: null; + // @ts-ignore assigned in parent constructor + challengedFormat: null; + battle: Battle = null!; + /** + * @return true to prevent line from being sent to server + */ + handleMessage(line: string) { + if (!line.startsWith('/') || line.startsWith('//')) return false; + const spaceIndex = line.indexOf(' '); + const cmd = spaceIndex >= 0 ? line.slice(1, spaceIndex) : line.slice(1); + const target = spaceIndex >= 0 ? line.slice(spaceIndex + 1) : ''; + switch (cmd) { + case 'play': { + this.battle.play(); + this.update(''); + return true; + } case 'pause': { + this.battle.pause(); + this.update(''); + return true; + } case 'ffto': case 'fastfowardto': { + let turnNum = Number(target); + if (target.charAt(0) === '+' || turnNum < 0) { + turnNum += this.battle.turn; + if (turnNum < 0) turnNum = 0; + } else if (target === 'end') { + turnNum = -1; + } + if (isNaN(turnNum)) { + this.receive(`|error|/ffto - Invalid turn number: ${target}`); + return true; + } + this.battle.fastForwardTo(turnNum); + this.update(''); + return true; + } case 'switchsides': { + this.battle.switchSides(); + return true; + }} + return super.handleMessage(line); + } +} + +class BattleDiv extends preact.Component { + shouldComponentUpdate() { + return false; + } + render() { + return
; + } +} + +class BattlePanel extends PSRoomPanel { + send = (text: string) => { + this.props.room.send(text); + }; + focus() { + this.base!.querySelector('textarea')!.focus(); + } + focusIfNoSelection = () => { + const selection = window.getSelection()!; + if (selection.type === 'Range') return; + this.focus(); + }; + onKey = (e: KeyboardEvent) => { + if (e.keyCode === 33) { // Pg Up key + const chatLog = this.base!.getElementsByClassName('chat-log')[0] as HTMLDivElement; + chatLog.scrollTop = chatLog.scrollTop - chatLog.offsetHeight + 60; + return true; + } else if (e.keyCode === 34) { // Pg Dn key + const chatLog = this.base!.getElementsByClassName('chat-log')[0] as HTMLDivElement; + chatLog.scrollTop = chatLog.scrollTop + chatLog.offsetHeight - 60; + return true; + } + return false; + }; + componentDidMount() { + const battle = new Battle($(this.base!).find('.battle'), $(this.base!).find('.battle-log')); + this.props.room.battle = battle; + battle.endCallback = () => this.forceUpdate(); + battle.play(); + super.componentDidMount(); + } + receive(line: string) { + if (line === `|initdone`) { + this.props.room.battle.fastForwardTo(-1); + return; + } + this.props.room.battle.add(line); + } + renderControls() { + const battle = this.props.room.battle; + if (!battle) return null; + const atEnd = battle.playbackState === Playback.Finished; + return ; + } + render() { + const room = this.props.room; + + return + + + {} + + + + {this.renderControls()} + ; + } +} + +PS.roomTypes['battle'] = { + Model: BattleRoom, + Component: BattlePanel, +}; +PS.updateRoomTypes(); diff --git a/src/panel-chat.tsx b/src/panel-chat.tsx index 33a0d1cb6..18fef5521 100644 --- a/src/panel-chat.tsx +++ b/src/panel-chat.tsx @@ -6,7 +6,7 @@ */ class ChatRoom extends PSRoom { - readonly classType: string = 'chat'; + readonly classType: 'chat' | 'battle' = 'chat'; users: {[userid: string]: string} = {}; userCount = 0; @@ -152,6 +152,7 @@ class ChatRoom extends PSRoom { class ChatTextEntry extends preact.Component<{ room: PSRoom, onMessage: (msg: string) => void, onKey: (e: KeyboardEvent) => boolean, + left?: number, }> { subscription: PSSubscription | null = null; textbox: HTMLTextAreaElement = null!; @@ -295,7 +296,9 @@ class ChatTextEntry extends preact.Component<{ return true; } render() { - return
+ return