diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index adfecc1..5400c1e 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -776,7 +776,7 @@ func (pR *Room) OnDismissed() { pR.RenderCacheSize = 256 pR.RenderFrameBuffer = NewRingBuffer(pR.RenderCacheSize) pR.DiscreteInputsBuffer = sync.Map{} - pR.InputsBuffer = NewRingBuffer((pR.RenderCacheSize >> 2)+1) + pR.InputsBuffer = NewRingBuffer((pR.RenderCacheSize >> 2) + 1) pR.LastAllConfirmedInputFrameId = -1 pR.LastAllConfirmedInputFrameIdWithChange = -1 @@ -1161,6 +1161,7 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende // TODO: Write unit-test for this function to compare with its frontend counter part func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSysMap map[int32]*resolv.Object) *RoomDownsyncFrame { + // TODO: Derive "nextRenderFramePlayers[*].CharacterState" as the frontend counter-part! nextRenderFramePlayers := make(map[int32]*PlayerDownsync, pR.Capacity) // Make a copy first for playerId, currPlayerDownsync := range currRenderFrame.Players { diff --git a/battle_srv/ws/serve.go b/battle_srv/ws/serve.go index 04de98c..4f6949f 100644 --- a/battle_srv/ws/serve.go +++ b/battle_srv/ws/serve.go @@ -271,7 +271,7 @@ func Serve(c *gin.Context) { VirtualGridToWorldRatio: pRoom.VirtualGridToWorldRatio, SpAtkLookupFrames: pRoom.SpAtkLookupFrames, - RenderCacheSize: pRoom.RenderCacheSize, + RenderCacheSize: pRoom.RenderCacheSize, } resp := &pb.WsResp{ diff --git a/frontend/assets/resources/pbfiles/room_downsync_frame.proto b/frontend/assets/resources/pbfiles/room_downsync_frame.proto index ce89922..88ad1cc 100644 --- a/frontend/assets/resources/pbfiles/room_downsync_frame.proto +++ b/frontend/assets/resources/pbfiles/room_downsync_frame.proto @@ -123,6 +123,8 @@ message MeleeBullet { int32 offenderJoinIndex = 16; int32 offenderPlayerId = 17; + + int32 offenderPlayerId = 17; } message RoomDownsyncFrame { diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index af774f2..5cbb696 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 342.07256941030164, + 210.4441731196186, 0, 0, 0, diff --git a/frontend/assets/scenes/offline_map_1.fire b/frontend/assets/scenes/offline_map_1.fire index cdea69f..eda2fde 100644 --- a/frontend/assets/scenes/offline_map_1.fire +++ b/frontend/assets/scenes/offline_map_1.fire @@ -453,7 +453,7 @@ "array": [ 0, 0, - 342.93857591513785, + 210.4441731196186, 0, 0, 0, diff --git a/frontend/assets/scripts/AttackingCharacter.js b/frontend/assets/scripts/AttackingCharacter.js index f098c5d..f49086f 100644 --- a/frontend/assets/scripts/AttackingCharacter.js +++ b/frontend/assets/scripts/AttackingCharacter.js @@ -38,28 +38,29 @@ cc.Class({ onLoad() { BaseCharacter.prototype.onLoad.call(this); - this.characterState = ATK_CHARACTER_STATE.Idle1[0]; }, - scheduleNewDirection(newScheduledDirection, forceAnimSwitch) { - const oldDx = this.activeDirection.dx, oldDy = this.activeDirection.dy; - BaseCharacter.prototype.scheduleNewDirection.call(this, newScheduledDirection, forceAnimSwitch); - if (ATK_CHARACTER_STATE.Atk1[0] == this.characterState) { - return; - } - - let newCharacterState = ATK_CHARACTER_STATE.Idle1[0]; - if (0 != newScheduledDirection.dx || 0 != newScheduledDirection.dy) { - newCharacterState = ATK_CHARACTER_STATE.Walking[0]; - } - - if (newCharacterState != this.characterState) { - this.characterState = newCharacterState; - const newAnimName = window.ATK_CHARACTER_STATE_ARR[newCharacterState][1]; - if (newAnimName != this.animComp.animationName) { - this.animComp.playAnimation(newAnimName); - // console.log(`JoinIndex=${this.joinIndex}, Resetting anim to ${newAnimName}, dir changed: (${oldDx}, ${oldDy}) -> (${newScheduledDirection.dx}, ${newScheduledDirection.dy})`); + updateCharacterAnim(newScheduledDirection, rdfPlayer, forceAnimSwitch) { + if (0 == rdfPlayer.framesToRecover) { + // Update directions + if (forceAnimSwitch || null == this.activeDirection || (null != newScheduledDirection && (newScheduledDirection.dx != this.activeDirection.dx || newScheduledDirection.dy != this.activeDirection.dy))) { + this.activeDirection = newScheduledDirection; + if (this.animComp && this.animComp.node) { + if (0 > newScheduledDirection.dx) { + this.animComp.node.scaleX = (-1.0); + } else if (0 < newScheduledDirection.dx) { + this.animComp.node.scaleX = (1.0); + } + } } } + + // Update per character state + let newCharacterState = rdfPlayer.characterState; + const newAnimName = window.ATK_CHARACTER_STATE_ARR[newCharacterState][1]; + if (newAnimName != this.animComp.animationName) { + this.animComp.playAnimation(newAnimName); + // console.log(`JoinIndex=${this.joinIndex}, Resetting anim to ${newAnimName}, dir changed: (${oldDx}, ${oldDy}) -> (${newScheduledDirection.dx}, ${newScheduledDirection.dy})`); + } }, }); diff --git a/frontend/assets/scripts/BaseCharacter.js b/frontend/assets/scripts/BaseCharacter.js index 3d4cf93..ae3c393 100644 --- a/frontend/assets/scripts/BaseCharacter.js +++ b/frontend/assets/scripts/BaseCharacter.js @@ -23,23 +23,6 @@ module.export = cc.Class({ self.ctrl = joystickInputControllerScriptIns; }, - scheduleNewDirection(newScheduledDirection, forceAnimSwitch) { - if (!newScheduledDirection) { - return; - } - - if (forceAnimSwitch || null == this.activeDirection || (newScheduledDirection.dx != this.activeDirection.dx || newScheduledDirection.dy != this.activeDirection.dy)) { - this.activeDirection = newScheduledDirection; - if (this.animComp && this.animComp.node) { - if (0 > newScheduledDirection.dx) { - this.animComp.node.scaleX = (-1.0); - } else if (0 < newScheduledDirection.dx) { - this.animComp.node.scaleX = (1.0); - } - } - } - }, - update(dt) {}, lateUpdate(dt) {}, diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index 7011f0d..75b79ef 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -309,7 +309,7 @@ cc.Class({ self.recentRenderCache = new RingBuffer(self.renderCacheSize); self.selfPlayerInfo = null; // This field is kept for distinguishing "self" and "others". - self.recentInputCache = new RingBuffer((self.renderCacheSize >> 2)+1); + self.recentInputCache = new RingBuffer((self.renderCacheSize >> 2) + 1); self.collisionSys = new collisions.Collisions(); @@ -768,18 +768,18 @@ cc.Class({ const newPlayerCollider = self.collisionSys.createPolygon(x0, y0, pts); const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; - newPlayerCollider.collisionPlayerIndex = collisionPlayerIndex; - newPlayerCollider.playerId = playerRichInfo.id; + newPlayerCollider.collisionPlayerIndex = parseInt(collisionPlayerIndex); + newPlayerCollider.playerId = parseInt(playerRichInfo.id); self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider); safelyAddChild(self.node, newPlayerNode); setLocalZOrder(newPlayerNode, 5); newPlayerNode.active = true; - playerScriptIns.scheduleNewDirection({ + playerScriptIns.updateCharacterAnim({ dx: playerRichInfo.dir.dx, dy: playerRichInfo.dir.dy - }, true); + }, playerRichInfo, true); return [newPlayerNode, playerScriptIns]; }, @@ -978,7 +978,9 @@ cc.Class({ // TODO: check "rdf.players[playerId].characterState" instead, might have to play Atk/Atked anim! if (null != delayedInputFrameForPrevRenderFrame) { const decodedInput = self.ctrl.decodeInput(delayedInputFrameForPrevRenderFrame.inputList[playerRichInfo.joinIndex - 1]); - playerRichInfo.scriptIns.scheduleNewDirection(decodedInput, false); + playerRichInfo.scriptIns.updateCharacterAnim(decodedInput, immediatePlayerInfo, false); + } else { + playerRichInfo.scriptIns.updateCharacterAnim(null, immediatePlayerInfo, false); } playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed); }); @@ -1012,49 +1014,174 @@ cc.Class({ dx: currPlayerDownsync.dir.dx, dy: currPlayerDownsync.dir.dy, }, + characterState: currPlayerDownsync.characterState, speed: currPlayerDownsync.speed, battleState: currPlayerDownsync.battleState, score: currPlayerDownsync.score, removed: currPlayerDownsync.removed, joinIndex: currPlayerDownsync.joinIndex, + framesToRecover: (0 < currPlayerDownsync.framesToRecover ? currPlayerDownsync.framesToRecover - 1 : 0), + hp: currPlayerDownsync.hp, + maxHp: currPlayerDownsync.maxHp, }; } const toRet = { id: currRenderFrame.id + 1, players: nextRenderFramePlayers, + meleeBullets: [] }; + const bulletPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order + const effPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order + for (let j in self.playerRichInfoArr) { + const joinIndex = parseInt(j) + 1; + bulletPushbacks[joinIndex - 1] = [0.0, 0.0]; + effPushbacks[joinIndex - 1] = [0.0, 0.0]; + const playerRichInfo = self.playerRichInfoArr[j]; + const playerId = playerRichInfo.id; + const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; + const playerCollider = collisionSysMap.get(collisionPlayerIndex); + const player = currRenderFrame.players[playerId]; + + const newVx = player.virtualGridX; + const newVy = player.virtualGridY; + const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[joinIndex - 1]); + playerCollider.x = newCpos[0]; + playerCollider.y = newCpos[1]; + } + + // Check bullet-anything collisions first, because the pushbacks caused by bullets might later be reverted by player-barrier collision + const colliderBullets = new Map(); // Will all be removed at the end of `applyInputFrameDownsyncDynamicsOnSingleRenderFrame` due to the need for being rollback-compatible + const removedBulletsAtCurrFrame = new Set(); + for (let k in currRenderFrame.meleeBullets) { + const meleeBullet = currRenderFrame.meleeBullets[k]; + if ( + meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames <= currRenderFrame.id + && + meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames + meleeBullet.activeFrames > currRenderFrame.id + ) { + const collisionBulletIndex = self.collisionBulletIndexPrefix + meleeBullet.battleLocalId; + const collisionOffenderIndex = self.collisionPlayerIndexPrefix + meleeBullet.offenderJoinIndex; + const offenderCollider = collisionSysMap.get(collisionOffenderIndex); + const offender = currRenderFrame.players[meleeBullet.offenderPlayerId]; + + const xfac = Math.sign(offender.dir.dx), + yfac = 0; // By now, straight Punch offset doesn't respect "y-axis" + const x0 = offenderCollider.x + xfac * meleeBullet.hitboxOffset, + y0 = offenderCollider.y + yfac * meleeBullet.hitboxOffset; + const pts = [[0, 0], [xfac * meleeBullet.hitboxSize.x, 0], [xfac * meleeBullet.hitboxSize.x, meleeBullet.hitboxSize.y], [0, meleeBullet.hitboxSize.y]]; + const newBulletCollider = collisionSys.createPolygon(x0, y0, pts); + newBulletCollider.collisionBulletIndex = collisionBulletIndex; + newBulletCollider.offenderPlayerId = meleeBullet.offenderPlayerId; + newBulletCollider.pushback = meleeBullet.pushback; + newBulletCollider.hitStunFrames = meleeBullet.hitStunFrames; + collisionSysMap.set(collisionBulletIndex, newBulletCollider); + colliderBullets.set(collisionBulletIndex, newBulletCollider); + console.log(`A meleeBullet=${JSON.stringify(meleeBullet)} is added to collisionSys at renderFrame.id=${currRenderFrame.id} as start-up frames ended and active frame is not yet ended`); + } + } + + collisionSys.update(); + const result1 = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle? + + colliderBullets.forEach((bulletCollider, collisionBulletIndex) => { + const potentials = bulletCollider.potentials(); + let shouldRemove = false; + for (const potential of potentials) { + if (null != potential.playerId && potential.playerId == bulletCollider.offenderPlayerId) continue; + if (!bulletCollider.collides(potential, result1)) continue; + if (null != potential.playerId) { + const joinIndex = potential.collisionPlayerIndex - self.collisionPlayerIndexPrefix; + bulletPushbacks[joinIndex - 1][0] += result1.overlap_x * bulletCollider.pushback; + bulletPushbacks[joinIndex - 1][1] += result1.overlap_y * bulletCollider.pushback; + const thatAckedPlayerInNextFrame = nextRenderFramePlayers[potential.playerId]; + thatAckedPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atked1[0]; + const oldFrameToRecover = thatAckedPlayerInNextFrame.framesToRecover; + thatAckedPlayerInNextFrame.framesToRecover = oldFrameToRecover > bulletCollider.hitStunFrames ? oldFrameToRecover : bulletCollider.hitStunFrames; // In case the hit player is already stun, we take the larger "hitStunFrames" + } + shouldRemove = true; + } + if (shouldRemove) { + removedBulletsAtCurrFrame.add(collisionBulletIndex); + } + }); + + for (let k in currRenderFrame.meleeBullets) { + const meleeBullet = currRenderFrame.meleeBullets[k]; + // [WARNING] remove from collisionSys ANYWAY for the convenience of rollback + const collisionBulletIndex = self.collisionBulletIndexPrefix + meleeBullet.battleLocalId; + if (collisionSysMap.has(collisionBulletIndex)) { + const bulletCollider = collisionSysMap.get(collisionBulletIndex); + bulletCollider.remove(); + collisionSysMap.delete(collisionBulletIndex); + } + if (removedBulletsAtCurrFrame.has(collisionBulletIndex)) continue; + toRet.meleeBullets.push(meleeBullet); + } + if (null != delayedInputFrame) { + const delayedInputFrameForPrevRenderFrame = self.getCachedInputFrameDownsyncWithPrediction(self._convertToInputFrameId(currRenderFrame.id - 1, self.inputDelayFrames)); const inputList = delayedInputFrame.inputList; - const effPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order + // Process player inputs for (let j in self.playerRichInfoArr) { const joinIndex = parseInt(j) + 1; effPushbacks[joinIndex - 1] = [0.0, 0.0]; - const playerId = self.playerRichInfoArr[j].id; + const playerRichInfo = self.playerRichInfoArr[j]; + const playerId = playerRichInfo.id; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); const player = currRenderFrame.players[playerId]; + if (0 < nextRenderFramePlayers[playerId].framesToRecover) { + // No need to process inputs for this player, but there might be bullet pushbacks on this player + if (0 != bulletPushbacks[joinIndex - 1][0] || 0 != bulletPushbacks[joinIndex - 1][1]) { + playerCollider.x += bulletPushbacks[joinIndex - 1][0]; + playerCollider.y += bulletPushbacks[joinIndex - 1][1]; + console.log(`playerId=${playerId}, joinIndex=${joinIndex} is pushbacked back by ${bulletPushbacks[joinIndex - 1]} by bullet impacts, now its framesToRecover is ${player.framesToRecover}`); + } + continue; + } const decodedInput = self.ctrl.decodeInput(inputList[joinIndex - 1]); - /* - Reset "position" of players in "collisionSys" according to "virtual grid position". The easy part is that we don't have path-dependent-integrals to worry about like that of thermal dynamics. + const prevDecodedInput = (null == delayedInputFrameForPrevRenderFrame ? null : self.ctrl.decodeInput(delayedInputFrameForPrevRenderFrame.inputList[joinIndex - 1])); + const prevBtnALevel = (null == prevDecodedInput ? 0 : prevDecodedInput.btnALevel); + + if (1 == decodedInput.btnALevel && 0 == prevBtnALevel) { + console.log(`playerId=${playerId} triggered a rising-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); + // The online map is not yet ready for bullet shooting! + /* + nextRenderFramePlayers[playerId].framesToRecover = PunchAtkConfig.recoveryFrames; + const punch = window.pb.protos.MeleeBullet.create(PunchAtkConfig); + punch.battleLocalId = self.bulletBattleLocalIdCounter++; + punch.offenderJoinIndex = joinIndex; + punch.offenderPlayerId = playerId; + punch.originatedRenderFrameId = currRenderFrame.id; + toRet.meleeBullets.push(punch); + console.log(`A rising-edge of meleeBullet=${JSON.stringify(punch)} is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); + + nextRenderFramePlayers[playerId].characterState = window.ATK_CHARACTER_STATE.Atk1[0]; */ - const newVx = player.virtualGridX + (decodedInput.dx + player.speed * decodedInput.dx); - const newVy = player.virtualGridY + (decodedInput.dy + player.speed * decodedInput.dy); - const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[joinIndex - 1]); - playerCollider.x = newCpos[0]; - playerCollider.y = newCpos[1]; - if (0 != decodedInput.dx || 0 != decodedInput.dy) { - // Update directions and thus would eventually update moving animation accordingly - nextRenderFramePlayers[playerId].dir.dx = decodedInput.dx; - nextRenderFramePlayers[playerId].dir.dy = decodedInput.dy; + } else if (0 == decodedInput.btnALevel && 1 == prevBtnALevel) { + console.log(`playerId=${playerId} triggered a falling-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); + } else { + // No trigger, process movement inputs + if (0 != decodedInput.dx || 0 != decodedInput.dy) { + // Update directions and thus would eventually update moving animation accordingly + nextRenderFramePlayers[playerId].dir.dx = decodedInput.dx; + nextRenderFramePlayers[playerId].dir.dy = decodedInput.dy; + nextRenderFramePlayers[playerId].characterState = window.ATK_CHARACTER_STATE.Walking[0]; + } else { + nextRenderFramePlayers[playerId].characterState = window.ATK_CHARACTER_STATE.Idle1[0]; + } + const movement = self.virtualGridToWorldPos(decodedInput.dx + player.speed * decodedInput.dx, decodedInput.dy + player.speed * decodedInput.dy); + playerCollider.x += movement[0]; + playerCollider.y += movement[1]; } } - collisionSys.update(); - const result = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle? + collisionSys.update(); // by now all "bulletCollider"s are removed + const result2 = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle? for (let j in self.playerRichInfoArr) { const joinIndex = parseInt(j) + 1; @@ -1064,10 +1191,10 @@ cc.Class({ const potentials = playerCollider.potentials(); for (const potential of potentials) { // Test if the player collides with the wall - if (!playerCollider.collides(potential, result)) continue; + if (!playerCollider.collides(potential, result2)) continue; // Push the player out of the wall - effPushbacks[joinIndex - 1][0] += result.overlap * result.overlap_x; - effPushbacks[joinIndex - 1][1] += result.overlap * result.overlap_y; + effPushbacks[joinIndex - 1][0] += result2.overlap * result2.overlap_x; + effPushbacks[joinIndex - 1][1] += result2.overlap * result2.overlap_y; } } @@ -1080,6 +1207,7 @@ cc.Class({ nextRenderFramePlayers[playerId].virtualGridX = newVpos[0]; nextRenderFramePlayers[playerId].virtualGridY = newVpos[1]; } + } return toRet; diff --git a/frontend/assets/scripts/OfflineMap.js b/frontend/assets/scripts/OfflineMap.js index 20f2b29..c77dd49 100644 --- a/frontend/assets/scripts/OfflineMap.js +++ b/frontend/assets/scripts/OfflineMap.js @@ -5,25 +5,25 @@ const OnlineMap = require('./Map'); const PunchAtkConfig = { // for offender - startupFrames: 2, - activeFrames: 2, - recoveryFrames: 4, // usually but not always "startupFrames+activeFrames" - recoveryFramesOnBlock: 4, // usually but not always the same as "recoveryFrames" - recoveryFramesOnHit: 4, // usually but not always the same as "recoveryFrames" + startupFrames: 18, + activeFrames: 42, + recoveryFrames: 60, // usually but not always "startupFrames+activeFrames" + recoveryFramesOnBlock: 60, // usually but not always the same as "recoveryFrames" + recoveryFramesOnHit: 60, // usually but not always the same as "recoveryFrames" moveforward: { x: 0, y: 0, }, hitboxOffset: 12.0, // should be about the radius of the PlayerCollider hitboxSize: { - x: 24.0, - y: 24.0, + x: 32.0, + y: 32.0, }, // for defender - hitStunFrames: 0, - blockStunFrames: 0, - pushback: 10.0, + hitStunFrames: 18, + blockStunFrames: 9, + pushback: 11.0, releaseTriggerType: 1, // 1: rising-edge, 2: falling-edge damage: 5 }; @@ -58,16 +58,18 @@ cc.Class({ const newPlayerCollider = self.collisionSys.createPolygon(x0, y0, pts); const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; + newPlayerCollider.collisionPlayerIndex = parseInt(collisionPlayerIndex); + newPlayerCollider.playerId = parseInt(playerRichInfo.id); self.collisionSysMap.set(collisionPlayerIndex, newPlayerCollider); safelyAddChild(self.node, newPlayerNode); setLocalZOrder(newPlayerNode, 5); newPlayerNode.active = true; - playerScriptIns.scheduleNewDirection({ + playerScriptIns.updateCharacterAnim({ dx: playerRichInfo.dir.dx, dy: playerRichInfo.dir.dy - }, true); + }, playerRichInfo, true); return [newPlayerNode, playerScriptIns]; }, @@ -178,6 +180,8 @@ cc.Class({ virtualGridY: 0, speed: 2 * self.worldToVirtualGridRatio, colliderRadius: 12, + characterState: window.ATK_CHARACTER_STATE.Idle1[0], + framesToRecover: 0, dir: { dx: 0, dy: 0 @@ -190,6 +194,8 @@ cc.Class({ virtualGridY: 40 * self.worldToVirtualGridRatio, speed: 2 * self.worldToVirtualGridRatio, colliderRadius: 12, + characterState: window.ATK_CHARACTER_STATE.Idle1[0], + framesToRecover: 0, dir: { dx: 0, dy: 0 @@ -295,21 +301,25 @@ cc.Class({ for (let k in currRenderFrame.meleeBullets) { const meleeBullet = currRenderFrame.meleeBullets[k]; if ( - meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames >= currRenderFrame.id + meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames <= currRenderFrame.id && - meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames + meleeBullet.activeFrames <= currRenderFrame.id + meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames + meleeBullet.activeFrames > currRenderFrame.id ) { - const collisionBulletIndex = self.collisionBulletIndexPrefix + melee.battleLocalId; - const collisionOffenderIndex = self.collisionPlayerIndexPrefix + melee.offenderJoinIndex; + const collisionBulletIndex = self.collisionBulletIndexPrefix + meleeBullet.battleLocalId; + const collisionOffenderIndex = self.collisionPlayerIndexPrefix + meleeBullet.offenderJoinIndex; const offenderCollider = collisionSysMap.get(collisionOffenderIndex); - const offender = currRenderFrame.players[melee.offenderPlayerId]; + const offender = currRenderFrame.players[meleeBullet.offenderPlayerId]; - const x0 = offenderCollider.x + offender.dir.dx * meleeBullet.hitboxOffset, - y0 = offenderCollider.y + offender.dir.dy * meleeBullet.hitboxOffset; - const pts = [[0, 0], [meleeBullet.hitboxSize.x, 0], [meleeBullet.hitboxSize.x, meleeBullet.hitboxSize.y], [0, meleeBullet.hitboxSize.y]]; + const xfac = Math.sign(offender.dir.dx), + yfac = 0; // By now, straight Punch offset doesn't respect "y-axis" + const x0 = offenderCollider.x + xfac * meleeBullet.hitboxOffset, + y0 = offenderCollider.y + yfac * meleeBullet.hitboxOffset; + const pts = [[0, 0], [xfac * meleeBullet.hitboxSize.x, 0], [xfac * meleeBullet.hitboxSize.x, meleeBullet.hitboxSize.y], [0, meleeBullet.hitboxSize.y]]; const newBulletCollider = collisionSys.createPolygon(x0, y0, pts); newBulletCollider.collisionBulletIndex = collisionBulletIndex; + newBulletCollider.offenderPlayerId = meleeBullet.offenderPlayerId; newBulletCollider.pushback = meleeBullet.pushback; + newBulletCollider.hitStunFrames = meleeBullet.hitStunFrames; collisionSysMap.set(collisionBulletIndex, newBulletCollider); colliderBullets.set(collisionBulletIndex, newBulletCollider); console.log(`A meleeBullet=${JSON.stringify(meleeBullet)} is added to collisionSys at renderFrame.id=${currRenderFrame.id} as start-up frames ended and active frame is not yet ended`); @@ -319,15 +329,16 @@ cc.Class({ collisionSys.update(); const result1 = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle? - colliderBullets.forEach((collisionBulletIndex, bulletCollider) => { + colliderBullets.forEach((bulletCollider, collisionBulletIndex) => { const potentials = bulletCollider.potentials(); let shouldRemove = false; for (const potential of potentials) { if (null != potential.playerId && potential.playerId == bulletCollider.offenderPlayerId) continue; if (!bulletCollider.collides(potential, result1)) continue; if (null != potential.playerId) { - bulletPushbacks[joinIndex - 1][0] += result1.overlap * result1.overlap_x * bulletCollider.pushback; - bulletPushbacks[joinIndex - 1][1] += result1.overlap * result1.overlap_y * bulletCollider.pushback; + const joinIndex = potential.collisionPlayerIndex - self.collisionPlayerIndexPrefix; + bulletPushbacks[joinIndex - 1][0] += result1.overlap_x * bulletCollider.pushback; + bulletPushbacks[joinIndex - 1][1] += result1.overlap_y * bulletCollider.pushback; const thatAckedPlayerInNextFrame = nextRenderFramePlayers[potential.playerId]; thatAckedPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atked1[0]; const oldFrameToRecover = thatAckedPlayerInNextFrame.framesToRecover; @@ -343,12 +354,13 @@ cc.Class({ for (let k in currRenderFrame.meleeBullets) { const meleeBullet = currRenderFrame.meleeBullets[k]; // [WARNING] remove from collisionSys ANYWAY for the convenience of rollback - if (collisionSysMap.has(meleeBullet.collisionBulletIndex)) { - const bulletCollider = collisionSysMap.get(meleeBullet.collisionBulletIndex); + const collisionBulletIndex = self.collisionBulletIndexPrefix + meleeBullet.battleLocalId; + if (collisionSysMap.has(collisionBulletIndex)) { + const bulletCollider = collisionSysMap.get(collisionBulletIndex); bulletCollider.remove(); - collisionSysMap.delete(meleeBullet.collisionBulletIndex); + collisionSysMap.delete(collisionBulletIndex); } - if (removedBulletsAtCurrFrame.has(meleeBullet)) continue; + if (removedBulletsAtCurrFrame.has(collisionBulletIndex)) continue; toRet.meleeBullets.push(meleeBullet); } @@ -364,8 +376,13 @@ cc.Class({ const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); const player = currRenderFrame.players[playerId]; - if (0 < player.framesToRecover) { - // No need to process inputs for this player + if (0 < nextRenderFramePlayers[playerId].framesToRecover) { + // No need to process inputs for this player, but there might be bullet pushbacks on this player + if (0 != bulletPushbacks[joinIndex - 1][0] || 0 != bulletPushbacks[joinIndex - 1][1]) { + playerCollider.x += bulletPushbacks[joinIndex - 1][0]; + playerCollider.y += bulletPushbacks[joinIndex - 1][1]; + console.log(`playerId=${playerId}, joinIndex=${joinIndex} is pushbacked back by ${bulletPushbacks[joinIndex - 1]} by bullet impacts, now its framesToRecover is ${player.framesToRecover}`); + } continue; } @@ -374,7 +391,6 @@ cc.Class({ const prevDecodedInput = (null == delayedInputFrameForPrevRenderFrame ? null : self.ctrl.decodeInput(delayedInputFrameForPrevRenderFrame.inputList[joinIndex - 1])); const prevBtnALevel = (null == prevDecodedInput ? 0 : prevDecodedInput.btnALevel); - const playerInNextFrame = nextRenderFramePlayers[playerId]; if (1 == decodedInput.btnALevel && 0 == prevBtnALevel) { console.log(`playerId=${playerId} triggered a rising-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); nextRenderFramePlayers[playerId].framesToRecover = PunchAtkConfig.recoveryFrames; @@ -386,20 +402,20 @@ cc.Class({ toRet.meleeBullets.push(punch); console.log(`A rising-edge of meleeBullet=${JSON.stringify(punch)} is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); - playerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atk1[0]; + nextRenderFramePlayers[playerId].characterState = window.ATK_CHARACTER_STATE.Atk1[0]; } else if (0 == decodedInput.btnALevel && 1 == prevBtnALevel) { console.log(`playerId=${playerId} triggered a falling-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); } else { // No trigger, process movement inputs if (0 != decodedInput.dx || 0 != decodedInput.dy) { // Update directions and thus would eventually update moving animation accordingly - playerInNextFrame.dir.dx = decodedInput.dx; - playerInNextFrame.dir.dy = decodedInput.dy; - playerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Walking[0]; + nextRenderFramePlayers[playerId].dir.dx = decodedInput.dx; + nextRenderFramePlayers[playerId].dir.dy = decodedInput.dy; + nextRenderFramePlayers[playerId].characterState = window.ATK_CHARACTER_STATE.Walking[0]; } else { - playerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0]; + nextRenderFramePlayers[playerId].characterState = window.ATK_CHARACTER_STATE.Idle1[0]; } - const movement = self.virtualGridToPlayerColliderPos(decodedInput.dx + player.speed * decodedInput.dx, decodedInput.dy + player.speed * decodedInput.dy, self.playerRichInfoArr[joinIndex - 1]); + const movement = self.virtualGridToWorldPos(decodedInput.dx + player.speed * decodedInput.dx, decodedInput.dy + player.speed * decodedInput.dy); playerCollider.x += movement[0]; playerCollider.y += movement[1]; }