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 + + {atEnd ? + Play + : battle.paused ? + Play + : + Pause + } {} + Last turn + Skip turn {} + First turn + Skip to end + + + Switch sides + + ; + } + 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 {PS.user.name}: { : null; - return + return - + {challengeTo || challengeFrom && [challengeTo, challengeFrom]} - + ; } } -class ChatUserList extends preact.Component<{room: ChatRoom}> { +class ChatUserList extends preact.Component<{room: ChatRoom, left?: number, minimized?: boolean}> { subscription: PSSubscription | null = null; componentDidMount() { this.subscription = this.props.room.subscribe(msg => { @@ -404,7 +407,7 @@ class ChatUserList extends preact.Component<{room: ChatRoom}> { function colorStyle(userid: ID) { return {color: BattleLog.usernameColor(userid)}; } - return + return {room.userCount} users {userList.map(([userid, name]) => { const groupSymbol = name.charAt(0); @@ -442,11 +445,14 @@ class ChatUserList extends preact.Component<{room: ChatRoom}> { class ChatLog extends preact.Component<{ class: string, room: ChatRoom, onClick?: (e: Event) => void, children?: preact.ComponentChildren, + left?: number, top?: number, noSubscription?: boolean; }> { - log: BattleLog = null!; + log: BattleLog | null = null; subscription: PSSubscription | null = null; componentDidMount() { - this.log = new BattleLog(this.base! as HTMLDivElement); + if (!this.props.noSubscription) { + this.log = new BattleLog(this.base! as HTMLDivElement); + } this.subscription = this.props.room.subscribe(msg => { if (!msg) return; const tokens = PS.lineParse(msg); @@ -470,7 +476,7 @@ class ChatLog extends preact.Component<{ this.props.room.renameUser(tokens[1], tokens[2]); break; } - this.log.add(tokens); + if (!this.props.noSubscription) this.log!.add(tokens); }); this.setControlsJSX(this.props.children); } @@ -481,25 +487,37 @@ class ChatLog extends preact.Component<{ if (props.class !== this.props.class) { this.base!.className = props.class; } + if (props.left !== this.props.left) this.base!.style.left = `${props.left || 0}px`; + if (props.top !== this.props.top) this.base!.style.top = `${props.top || 0}px`; this.setControlsJSX(props.children); - this.log.updateScroll(); + this.updateScroll(); return false; } setControlsJSX(jsx: preact.ComponentChildren | undefined) { const children = this.base!.children; - let controlsElem: HTMLDivElement | undefined = children[children.length - 1] as HTMLDivElement; - if (controlsElem.className !== 'controls') controlsElem = undefined; + let controlsElem = children[children.length - 1] as HTMLDivElement | undefined; + if (controlsElem && controlsElem.className !== 'controls') controlsElem = undefined; if (!jsx) { if (!controlsElem) return; preact.render(null, this.base!, controlsElem); - this.log.updateScroll(); + this.updateScroll(); return; } preact.render({jsx}, this.base!, controlsElem); - this.log.updateScroll(); + this.updateScroll(); + } + updateScroll() { + if (this.log) { + this.log.updateScroll(); + } else if (this.props.room.battle) { + this.log = (this.props.room.battle as Battle).scene.log; + this.log.updateScroll(); + } } render() { - return ; + return ; } } diff --git a/src/panel-mainmenu.tsx b/src/panel-mainmenu.tsx index ca0571c85..4e093706b 100644 --- a/src/panel-mainmenu.tsx +++ b/src/panel-mainmenu.tsx @@ -77,7 +77,7 @@ class MainMenuRoom extends PSRoom { continue; } room.challengedFormat = json.challengesFrom[targetUserid] || null; - room.challengingFormat = json.challengeTo.to === targetUserid ? json.challengeTo.format : null; + room.challengingFormat = (json.challengeTo || {}).to === targetUserid ? json.challengeTo.format : null; room.update(''); } } diff --git a/style/client2.css b/style/client2.css index 4cfa7af0d..6ee8408ba 100644 --- a/style/client2.css +++ b/style/client2.css @@ -1567,7 +1567,6 @@ a.ilink.yours { text-align: left; } .userlist li { - border-bottom: 1px solid #CCCCCC; height: 19px; font: 10pt Verdana, sans-serif; white-space: nowrap; @@ -1669,7 +1668,6 @@ a.ilink.yours { top: 370px; left: 0; width: 640px; - background: #EEF2F5; } .battle-chat-toggle { display: none; @@ -3180,8 +3178,7 @@ a.ilink.yours { color: #DDD; } .dark .ps-room.ps-room-opaque, -.dark .battle-log-add, -.dark .battle-controls { +.dark .battle-log-add { background: #444444; color: #DDD; } @@ -3301,9 +3298,6 @@ a.ilink.yours { .dark .userlist span { text-shadow: 1px 1px 0 #222222; } -.dark .userlist li { - border-bottom-color: #444; -} .dark .userlist li button:hover { background: #222222; } diff --git a/testclient-beta.html b/testclient-beta.html index a9690f211..320bb5435 100644 --- a/testclient-beta.html +++ b/testclient-beta.html @@ -69,6 +69,15 @@ + + + + + + + + +
+ {atEnd ? + Play + : battle.paused ? + Play + : + Pause + } {} + Last turn + Skip turn {} + First turn + Skip to end +
+ Switch sides +