diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index f368a31..e6855ef 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -754,7 +754,7 @@ func (pR *Room) OnDismissed() { pR.InputFrameUpsyncDelayTolerance = 2 pR.MaxChasingRenderFramesPerUpdate = 8 - pR.BackendDynamicsEnabled = true // [WARNING] When "false", recovery upon reconnection wouldn't work! + pR.BackendDynamicsEnabled = false // [WARNING] When "false", recovery upon reconnection wouldn't work! punchSkillId := int32(1) pR.MeleeSkillConfig = make(map[int32]*MeleeBullet, 0) pR.MeleeSkillConfig[punchSkillId] = &MeleeBullet{ @@ -782,6 +782,12 @@ func (pR *Room) OnDismissed() { Damage: int32(5), } + pR.SnapIntoPlatformOverlap = float64(0.1) + pR.SnapIntoPlatformThreshold = float64(0.5) + pR.JumpingInitVelY = int32(float64(6) * pR.WorldToVirtualGridRatio) + pR.GravityX = 0 + pR.GravityY = -(4*pR.JumpingInitVelY + (pR.ServerFps - 1)) / pR.ServerFps // i.e. -Math.ceil(4*jumpingInitVelY / serverFps) + pR.ChooseStage() pR.EffectivePlayerCount = 0 @@ -1502,10 +1508,12 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF func (pR *Room) decodeInput(encodedInput uint64) *InputFrameDecoded { encodedDirection := (encodedInput & uint64(15)) btnALevel := int32((encodedInput >> 4) & 1) + btnBLevel := int32((encodedInput >> 5) & 1) return &InputFrameDecoded{ Dx: DIRECTION_DECODER[encodedDirection][0], Dy: DIRECTION_DECODER[encodedDirection][1], BtnALevel: btnALevel, + BtnBLevel: btnBLevel, } } diff --git a/battle_srv/ws/serve.go b/battle_srv/ws/serve.go index c0131c5..e8de933 100644 --- a/battle_srv/ws/serve.go +++ b/battle_srv/ws/serve.go @@ -265,9 +265,14 @@ func Serve(c *gin.Context) { WorldToVirtualGridRatio: pRoom.WorldToVirtualGridRatio, VirtualGridToWorldRatio: pRoom.VirtualGridToWorldRatio, - SpAtkLookupFrames: pRoom.SpAtkLookupFrames, - RenderCacheSize: pRoom.RenderCacheSize, - MeleeSkillConfig: pRoom.MeleeSkillConfig, + SpAtkLookupFrames: pRoom.SpAtkLookupFrames, + RenderCacheSize: pRoom.RenderCacheSize, + MeleeSkillConfig: pRoom.MeleeSkillConfig, + SnapIntoPlatformOverlap: pRoom.SnapIntoPlatformOverlap, + SnapIntoPlatformThreshold: pRoom.SnapIntoPlatformThreshold, + JumpingInitVelY: pRoom.JumpingInitVelY, + GravityX: pRoom.GravityX, + GravityY: pRoom.GravityY, } resp := &pb.WsResp{ diff --git a/frontend/assets/resources/map/dungeon/map.tmx b/frontend/assets/resources/map/dungeon/map.tmx index b257153..ed64c28 100644 --- a/frontend/assets/resources/map/dungeon/map.tmx +++ b/frontend/assets/resources/map/dungeon/map.tmx @@ -1,18 +1,18 @@ - + - eJzt3D9v00AYwGErFUuHigqB1J0Bic+BhDqgTnRj6gdBYmdkZILviSNiFKwkTlz73rPfZ3gUKcmQ3O/sXP5crpqmuQIAAAAAAAAAAAAAAAAAgBGud6IfB+U87en6PyUU3SGy/2an6/+ldX/EZoX0/7//Mfqvz37/+4H+1xW00n++/ofov27b5/5659DY9M8J0a30L9s/A/31j+4Q2f/FCNHN9Ndff/31j+lfA/31119//fXXf1n9v595v9sd/dfV/1fr55Hbtt8//2590j+8/9gx7T5T/PZM+uuvv/7bx3LXo//6+9/1+jv+9dc/vv/LAVP236d/bP+h7of6/2h9vlDX3/qvnv7ntn9orP+X4tz+57bf6L8oQ/0PNX7fujnSvt+/7+FC+sf1P9T+Zk+/+xz937Te6l9N/1Pn/f3+U9O/3v5j1oiX0r98/3PmQH8sp+w7ZMr+mwpa1Nh/qjE+ZUx7/dfTP5r+ueeA/vrrr390i6j+0eMfLfv7v+jxj6Z/bvrnpn9u+uemf27656Z/bvrnpn9u+uemf27656Z/bvrnpn9u+o/3qie6pf7L7n87YI7fqOlfT3/Hf939+731XzbrP/1Lj/ex1/bI/psKWtTc/3HnY9N8/fD38t91j4Ht9C/Tv+ve182D6Ib6z9f/1N6gofdrkef1S/pHd6i1f9f53TPUvodM/3nVvo8we/+593zWvpc0e/9Sx3+nts+J9C/bv5sDl9K/XP8p1/Nj/ttH/3Uf/yX/30n/ZfYvNQ/0zy17f+I7AAAAAAAAAAAAAAAAAOX9ARz6kGc= + eJzt3DFv00AYgGErFUuHigqBVGYGJP4ECxLqgDrRjak/BImdkZGJ/k8cNUbBSmLnavu75HuGR5WaDum9Z+fi5HzRNM0FAAAAAAAAAAAAAAAAAAAUuNyIfh4s52FL1/8hoegOkf1XG13/b63bPVZnSP//+++j//nZ7n870P+yglb6z9d/F/3P2/p/f72xa2z654ToVvov2z8D/fWP7hDZ/0WB6Gb666+//vrH9K+B/vrrr7/++ut/Wv1/jvy76w39z6v/n9bvPY+tP39+bH3RP7x/6Zh21xR/PJP++uuv//q53PTof/79b3r9Hf/66x/f/+WAKftv0z+2/1D3Xf1/tb4eqetv/VdP/7Ht7xrr/1Mxtv/Y9iv9T8pQ/12NP7Su9rTv9++7O5L+cf13tb/a0u8+R/83rXf6V9P/0Hl/u//U9K+3f8ka8Vj6L99/zBzoj+WUfYdM2X9VQYsa+081xoeUtNf/fPpH0z/3HNBff/31j24R1T96/KNlf/8XPf7R9M9N/9z0z03/3PTPTf/c9M9N/9z0z03/3PTPTf/c9M9N/9z0z03/cq96olvqf9r9rwfM8R01/evp7/ivu3+/t/6nzfpP/6XHe99re2T/VQUtau5/v/G5ab5/evr573f3ge30X6Z/172vmwfRDfWfr/+hvUFD79ciz+vH9I/uUGv/rvP7Z6h9D5n+86p9H2H2/nPv+ax9L2n2/ksd/53arhPpv2z/bg4cS//l+k+5ni+5t4/+9Rz/z50Hpfd3Wup1Qv9xSufB28L+j83T9SX96+hfqu3/sbR/Z855kL0/8R0AAAAAAAAAAAAAAACA5f0FyTyU0g== - + - + @@ -175,12 +175,12 @@ - + - + @@ -195,7 +195,7 @@ - + @@ -205,11 +205,6 @@ - - - - - @@ -269,5 +264,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/assets/resources/prefabs/ControlledCharacter.prefab b/frontend/assets/resources/prefabs/ControlledCharacter.prefab index 915d81b..c9b4bb1 100644 --- a/frontend/assets/resources/prefabs/ControlledCharacter.prefab +++ b/frontend/assets/resources/prefabs/ControlledCharacter.prefab @@ -408,7 +408,7 @@ "ctor": "Float64Array", "array": [ 3, - 20, + 40, 0, 0, 0, diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index 31ccba3..8e744bd 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 209.73151519075364, + 215.64032554232523, 0, 0, 0, diff --git a/frontend/assets/scenes/offline_map_1.fire b/frontend/assets/scenes/offline_map_1.fire index 9e6102e..840a0db 100644 --- a/frontend/assets/scenes/offline_map_1.fire +++ b/frontend/assets/scenes/offline_map_1.fire @@ -454,7 +454,7 @@ "array": [ 0, 0, - 209.73151519075364, + 215.64032554232523, 0, 0, 0, diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index 8f38923..7f43ee1 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -402,6 +402,7 @@ cc.Class({ console.log(`Received parsedBattleColliderInfo via ws`); // TODO: Upon reconnection, the backend might have already been sending down data that'd trigger "onRoomDownsyncFrame & onInputFrameDownsyncBatch", but frontend could reject those data due to "battleState != PlayerBattleState.ACTIVE". Object.assign(self, parsedBattleColliderInfo); + self.gravityX = parsedBattleColliderInfo.gravityX; // to avoid integer default value 0 accidentally becoming null in "Object.assign(...)" self.tooFastDtIntervalMillis = 0.5 * self.rollbackEstimatedDtMillis; const tiledMapIns = self.node.getComponent(cc.TiledMap); @@ -834,6 +835,7 @@ cc.Class({ */ // [WARNING] Don't try to get "prevRdf(i.e. renderFrameId == latest-1)" by "self.recentRenderCache.getByFrameId(...)" here, as the cache might have been updated by asynchronous "onRoomDownsyncFrame(...)" calls! self.applyRoomDownsyncFrameDynamics(rdf, prevRdf); + self.showDebugBoundaries(); ++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!! self.lastRenderFrameIdTriggeredAt = performance.now(); let t3 = performance.now(); @@ -978,6 +980,73 @@ cc.Class({ } }, + showDebugBoundaries() { + const self = this; + if (self.showCriticalCoordinateLabels) { + let g = self.g; + g.clear(); + + for (let k in self.collisionSys._bvh._bodies) { + const body = self.collisionSys._bvh._bodies[k]; + if (!body._polygon) continue; + if (null != body.data && null != body.data.joinIndex) { + // character + if (1 == body.data.joinIndex) { + g.strokeColor = cc.Color.BLUE; + } else { + g.strokeColor = cc.Color.RED; + } + } else { + // barrier + g.strokeColor = cc.Color.WHITE; + } + g.moveTo(body.x, body.y); + const cnt = body._coords.length; + for (let j = 0; j < cnt; j += 2) { + const x = body._coords[j], + y = body._coords[j + 1]; + g.lineTo(x, y); + } + g.lineTo(body.x, body.y); + g.stroke(); + } + // For convenience of recovery upon reconnection, active bullets are always created & immediately removed from "collisionSys" within "applyInputFrameDownsyncDynamicsOnSingleRenderFrame" + + for (let k in rdf.meleeBullets) { + const meleeBullet = rdf.meleeBullets[k]; + if ( + meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames <= rdf.id + && + meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames + meleeBullet.activeFrames > rdf.id + ) { + const offender = rdf.players[meleeBullet.offenderPlayerId]; + if (1 == offender.joinIndex) { + g.strokeColor = cc.Color.BLUE; + } else { + g.strokeColor = cc.Color.RED; + } + + let xfac = 1; // By now, straight Punch offset doesn't respect "y-axis" + if (0 > offender.dirX) { + xfac = -1; + } + const [offenderWx, offenderWy] = self.virtualGridToWorldPos(offender.virtualGridX, offender.virtualGridY); + const bulletWx = offenderWx + xfac * meleeBullet.hitboxOffset; + const bulletWy = offenderWy + 0.5 * meleeBullet.hitboxSize.y; + const [bulletCx, bulletCy] = self.worldToPolygonColliderAnchorPos(bulletWx, bulletWy, meleeBullet.hitboxSize.x * 0.5, meleeBullet.hitboxSize.y * 0.5), + pts = [[0, 0], [meleeBullet.hitboxSize.x, 0], [meleeBullet.hitboxSize.x, meleeBullet.hitboxSize.y], [0, meleeBullet.hitboxSize.y]]; + + g.moveTo(bulletCx, bulletCy); + for (let j = 0; j < pts.length; j += 1) { + g.lineTo(pts[j][0] + bulletCx, pts[j][1] + bulletCy); + } + g.lineTo(bulletCx, bulletCy); + g.stroke(); + } + } + } + }, + getCachedInputFrameDownsyncWithPrediction(inputFrameId) { const self = this; const inputFrameDownsync = self.recentInputCache.getByFrameId(inputFrameId); @@ -1004,7 +1073,10 @@ cc.Class({ virtualGridY: currPlayerDownsync.virtualGridY, dirX: currPlayerDownsync.dirX, dirY: currPlayerDownsync.dirY, + velX: currPlayerDownsync.velX, + velY: currPlayerDownsync.velY, characterState: currPlayerDownsync.characterState, + inAir: true, // will be updated if collided with a barrier with "0 > pushbackY" speed: currPlayerDownsync.speed, battleState: currPlayerDownsync.battleState, score: currPlayerDownsync.score, @@ -1018,12 +1090,17 @@ cc.Class({ const nextRenderFrameMeleeBullets = []; - 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 + // Guaranteed determinism regardless of traversal order + const jumpTriggered = new Array(self.playerRichInfoArr.length); + const movements = new Array(self.playerRichInfoArr.length); + const bulletPushbacks = new Array(self.playerRichInfoArr.length); + const effPushbacks = new Array(self.playerRichInfoArr.length); // Reset playerCollider position from the "virtual grid position" for (let j in self.playerRichInfoArr) { const joinIndex = parseInt(j) + 1; + jumpTriggered[joinIndex - 1] = false; + movements[joinIndex - 1] = [0.0, 0.0]; bulletPushbacks[joinIndex - 1] = [0.0, 0.0]; effPushbacks[joinIndex - 1] = [0.0, 0.0]; const playerRichInfo = self.playerRichInfoArr[j]; @@ -1031,10 +1108,20 @@ cc.Class({ const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); const currPlayerDownsync = currRenderFrame.players[playerId]; + const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; const newVx = currPlayerDownsync.virtualGridX; const newVy = currPlayerDownsync.virtualGridY; [playerCollider.x, playerCollider.y] = self.virtualGridToPolygonColliderAnchorPos(newVx, newVy, self.playerRichInfoArr[joinIndex - 1].colliderRadius, self.playerRichInfoArr[joinIndex - 1].colliderRadius); + + // Process gravity before anyother interaction, by now "currPlayerDownsync.velX & velY" are properly snapped to be parallel to the edge of its standing platform if necessary + [movements[joinIndex - 1][0], movements[joinIndex - 1][1]] = self.virtualGridToWorldPos(currPlayerDownsync.velX, currPlayerDownsync.velY); + playerCollider.x += movements[joinIndex - 1][0]; + playerCollider.y += movements[joinIndex - 1][1]; + if (currPlayerDownsync.inAir) { + thatPlayerInNextFrame.velX += self.gravityX; + thatPlayerInNextFrame.velY += self.gravityY; + } } // Check bullet-anything collisions first, because the pushbacks caused by bullets might later be reverted by player-barrier collision @@ -1087,8 +1174,12 @@ cc.Class({ } bulletPushbacks[joinIndex - 1][0] += xfac * bulletCollider.data.pushback; // Only for straight punch, there's no y-pushback bulletPushbacks[joinIndex - 1][1] += 0; + const thatAckedPlayerInCurFrame = currRenderFrame.players[potential.data.id]; const thatAckedPlayerInNextFrame = nextRenderFramePlayers[potential.data.id]; thatAckedPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atked1[0]; + if (thatAckedPlayerInCurFrame.inAir) { + thatAckedPlayerInNextFrame.characterState = window.toInAirConjugate(thatAckedPlayerInNextFrame.characterState); + } const oldFramesToRecover = thatAckedPlayerInNextFrame.framesToRecover; thatAckedPlayerInNextFrame.framesToRecover = (oldFramesToRecover > bulletCollider.data.hitStunFrames ? oldFramesToRecover : bulletCollider.data.hitStunFrames); // In case the hit player is already stun, we extend it } @@ -1136,9 +1227,25 @@ cc.Class({ } const decodedInput = self.ctrl.decodeInput(inputList[joinIndex - 1]); - const prevDecodedInput = (null == delayedInputFrameForPrevRenderFrame ? null : self.ctrl.decodeInput(delayedInputFrameForPrevRenderFrame.inputList[joinIndex - 1])); const prevBtnALevel = (null == prevDecodedInput ? 0 : prevDecodedInput.btnALevel); + const prevBtnBLevel = (null == prevDecodedInput ? 0 : prevDecodedInput.btnBLevel); + /* + [WARNING] Player input alone WOULD NOT take "characterState" into any "ATK_CHARACTER_STATE_IN_AIR_SET", only after the calculation of "effPushbacks" do we know exactly whether or not a player is "inAir", the finalize the transition of "thatPlayerInNextFrame.characterState". + */ + if (1 == decodedInput.btnBLevel && 0 == prevBtnBLevel) { + const characStateAlreadyInAir = window.ATK_CHARACTER_STATE_IN_AIR_SET.has(thatPlayerInNextFrame.characterState); + const characStateIsInterruptWaivable = window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(thatPlayerInNextFrame.characterState); + if ( + !characStateAlreadyInAir + && + characStateIsInterruptWaivable + ) { + thatPlayerInNextFrame.velY = self.jumpingInitVelY; + jumpTriggered[joinIndex - 1] = true; + console.log(`playerId=${playerId}, joinIndex=${joinIndex} triggered a rising-edge of btnB at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}, nextVelY=${thatPlayerInNextFrame.velY}, characStateAlreadyInAir=${characStateAlreadyInAir}, characStateIsInterruptWaivable=${characStateIsInterruptWaivable}`); + } + } 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}`); @@ -1155,52 +1262,91 @@ cc.Class({ // console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atk1[0]; + if (false == currPlayerDownsync.inAir) { + thatPlayerInNextFrame.velX = 0; // prohibits simultaneous movement with Atk1 on the ground + } } } 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 bullet trigger, process movement inputs + // No bullet trigger, process joystick movement inputs (except for jumping). if (0 != decodedInput.dx || 0 != decodedInput.dy) { // Update directions and thus would eventually update moving animation accordingly thatPlayerInNextFrame.dirX = decodedInput.dx; thatPlayerInNextFrame.dirY = decodedInput.dy; + thatPlayerInNextFrame.velX = decodedInput.dx * currPlayerDownsync.speed; thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Walking[0]; } else { thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0]; + thatPlayerInNextFrame.velX = 0; } - const [movementX, movementY] = self.virtualGridToWorldPos(decodedInput.dx + currPlayerDownsync.speed * decodedInput.dx, decodedInput.dy + currPlayerDownsync.speed * decodedInput.dy); - playerCollider.x += movementX; - playerCollider.y += movementY; + } + if (currPlayerDownsync.inAir) { + thatPlayerInNextFrame.characterState = window.toInAirConjugate(thatPlayerInNextFrame.characterState); } } + } - collisionSys.update(); // by now all "bulletCollider"s are removed - const result2 = 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; - const playerId = self.playerRichInfoArr[j].id; - const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; - const playerCollider = collisionSysMap.get(collisionPlayerIndex); - const potentials = playerCollider.potentials(); - for (const potential of potentials) { - // Test if the player collides with the wall - if (!playerCollider.collides(potential, result2)) continue; - // Push the player out of the wall - effPushbacks[joinIndex - 1][0] += result2.overlap * result2.overlap_x; - effPushbacks[joinIndex - 1][1] += result2.overlap * result2.overlap_y; + for (let j in self.playerRichInfoArr) { + const joinIndex = parseInt(j) + 1; + const playerId = self.playerRichInfoArr[j].id; + const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; + const playerCollider = collisionSysMap.get(collisionPlayerIndex); + const potentials = playerCollider.potentials(); + const currPlayerDownsync = currRenderFrame.players[playerId]; + const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; + let fallStopping = false; + let [snappedIntoPlatformEx, snappedIntoPlatformEy] = [null, null]; + for (const potential of potentials) { + // Test if the player collides with the wall + if (!playerCollider.collides(potential, result2)) continue; + // Push the player out of the wall + let [pushbackX, pushbackY] = [result2.overlap * result2.overlap_x, result2.overlap * result2.overlap_y]; + if (null == potential.data) { + // "null == potential.data" implies a barrier + const normAlignmentWithGravity = (result2.overlap_x * 0 + result2.overlap_y * (-1.0)); + const flatEnough = (self.snapIntoPlatformThreshold < normAlignmentWithGravity); // prevents false snapping on the lateral sides + const remainsNotInAir = (!currPlayerDownsync.inAir && flatEnough); + const localFallStopping = (currPlayerDownsync.inAir && flatEnough); + if (remainsNotInAir || localFallStopping) { + fallStopping |= localFallStopping; + [pushbackX, pushbackY] = [(result2.overlap - self.snapIntoPlatformOverlap) * result2.overlap_x, (result2.overlap - self.snapIntoPlatformOverlap) * result2.overlap_y] + // [overlay_x, overlap_y] is the unit vector that points into the platform; FIXME: Should only assign to [snappedIntoPlatformEx, snappedIntoPlatformEy] at most once! + snappedIntoPlatformEx = -result2.overlap_y; + snappedIntoPlatformEy = result2.overlap_x; + if (snappedIntoPlatformEx * currPlayerDownsync.dirX + snappedIntoPlatformEy * currPlayerDownsync.dirY) { + [snappedIntoPlatformEx, snappedIntoPlatformEy] = [-snappedIntoPlatformEx, -snappedIntoPlatformEy]; + } + } } + // What if we're on the edge of 2 barriers? Would adding up make an unexpected bounce? + effPushbacks[joinIndex - 1][0] += pushbackX; + effPushbacks[joinIndex - 1][1] += pushbackY; } - - for (let j in self.playerRichInfoArr) { - const joinIndex = parseInt(j) + 1; - const playerId = self.playerRichInfoArr[j].id; - const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; - const playerCollider = collisionSysMap.get(collisionPlayerIndex); - const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; - [thatPlayerInNextFrame.virtualGridX, thatPlayerInNextFrame.virtualGridY] = self.polygonColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[joinIndex - 1][0], playerCollider.y - effPushbacks[joinIndex - 1][1], self.playerRichInfoArr[j].colliderRadius, self.playerRichInfoArr[j].colliderRadius); + if (false == jumpTriggered[joinIndex - 1] && null != snappedIntoPlatformEx && null != snappedIntoPlatformEy) { + thatPlayerInNextFrame.inAir = false; + if (fallStopping) { + thatPlayerInNextFrame.velY = 0; + thatPlayerInNextFrame.velX = 0; + thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0]; + thatPlayerInNextFrame.framesToRecover = 0; + } + const dotProd = thatPlayerInNextFrame.velX * snappedIntoPlatformEx + thatPlayerInNextFrame.velY * snappedIntoPlatformEy; + [thatPlayerInNextFrame.velX, thatPlayerInNextFrame.velY] = [dotProd * snappedIntoPlatformEx, dotProd * snappedIntoPlatformEy]; } + } + // Get players out of stuck barriers if there's any + for (let j in self.playerRichInfoArr) { + const joinIndex = parseInt(j) + 1; + const playerId = self.playerRichInfoArr[j].id; + const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; + const playerCollider = collisionSysMap.get(collisionPlayerIndex); + const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; + [thatPlayerInNextFrame.virtualGridX, thatPlayerInNextFrame.virtualGridY] = self.polygonColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[joinIndex - 1][0], playerCollider.y - effPushbacks[joinIndex - 1][1], self.playerRichInfoArr[j].colliderRadius, self.playerRichInfoArr[j].colliderRadius); } return window.pb.protos.RoomDownsyncFrame.create({ diff --git a/frontend/assets/scripts/OfflineMap.js b/frontend/assets/scripts/OfflineMap.js index ab14a1b..2128051 100644 --- a/frontend/assets/scripts/OfflineMap.js +++ b/frontend/assets/scripts/OfflineMap.js @@ -39,6 +39,7 @@ cc.Class({ self.rollbackEstimatedDt = 0.016667; self.rollbackEstimatedDtMillis = 16.667; self.rollbackEstimatedDtNanos = 16666666; + self.tooFastDtIntervalMillis = 0.5 * self.rollbackEstimatedDtMillis; self.worldToVirtualGridRatio = 1000; self.virtualGridToWorldRatio = 1.0 / self.worldToVirtualGridRatio; @@ -206,7 +207,8 @@ cc.Class({ const self = this; if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) { const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt; - if (elapsedMillisSinceLastFrameIdTriggered < (self.rollbackEstimatedDtMillis)) { + if (elapsedMillisSinceLastFrameIdTriggered < self.tooFastDtIntervalMillis) { + // [WARNING] We should avoid a frontend ticking too fast to prevent cheating, as well as ticking too slow to cause a "resync avalanche" that impacts user experience! // console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered); return; } @@ -223,373 +225,12 @@ cc.Class({ const [prevRdf, rdf] = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.collisionSys, self.collisionSysMap, false); self.applyRoomDownsyncFrameDynamics(rdf, prevRdf); + self.showDebugBoundaries(); + ++self.renderFrameId; + self.lastRenderFrameIdTriggeredAt = performance.now(); let t3 = performance.now(); } catch (err) { console.error("Error during Map.update", err); - } finally { - ++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!! - } - } - }, - - applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap) { - const self = this; - const nextRenderFramePlayers = {}; - for (let playerId in currRenderFrame.players) { - const currPlayerDownsync = currRenderFrame.players[playerId]; - nextRenderFramePlayers[playerId] = { - id: playerId, - virtualGridX: currPlayerDownsync.virtualGridX, - virtualGridY: currPlayerDownsync.virtualGridY, - dirX: currPlayerDownsync.dirX, - dirY: currPlayerDownsync.dirY, - velX: currPlayerDownsync.velX, - velY: currPlayerDownsync.velY, - characterState: currPlayerDownsync.characterState, - inAir: true, // will be updated if collided with a barrier with "0 > pushbackY" - 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 nextRenderFrameMeleeBullets = []; - - // Guaranteed determinism regardless of traversal order - const jumpTriggered = new Array(self.playerRichInfoArr.length); - const movements = new Array(self.playerRichInfoArr.length); - const bulletPushbacks = new Array(self.playerRichInfoArr.length); - const effPushbacks = new Array(self.playerRichInfoArr.length); - - // Reset playerCollider position from the "virtual grid position" - for (let j in self.playerRichInfoArr) { - const joinIndex = parseInt(j) + 1; - jumpTriggered[joinIndex - 1] = false; - movements[joinIndex - 1] = [0.0, 0.0]; - 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 currPlayerDownsync = currRenderFrame.players[playerId]; - const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; - - const newVx = currPlayerDownsync.virtualGridX; - const newVy = currPlayerDownsync.virtualGridY; - [playerCollider.x, playerCollider.y] = self.virtualGridToPolygonColliderAnchorPos(newVx, newVy, self.playerRichInfoArr[joinIndex - 1].colliderRadius, self.playerRichInfoArr[joinIndex - 1].colliderRadius); - - // Process gravity before anyother interaction, by now "currPlayerDownsync.velX & velY" are properly snapped to be parallel to the edge of its standing platform if necessary - [movements[joinIndex - 1][0], movements[joinIndex - 1][1]] = self.virtualGridToWorldPos(currPlayerDownsync.velX, currPlayerDownsync.velY); - playerCollider.x += movements[joinIndex - 1][0]; - playerCollider.y += movements[joinIndex - 1][1]; - if (currPlayerDownsync.inAir) { - thatPlayerInNextFrame.velX += self.gravityX; - thatPlayerInNextFrame.velY += self.gravityY; - } - } - - // Check bullet-anything collisions first, because the pushbacks caused by bullets might later be reverted by player-barrier collision - const bulletColliders = 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]; - - let xfac = 1; // By now, straight Punch offset doesn't respect "y-axis" - if (0 > offender.dirX) { - xfac = -1; - } - const [offenderWx, offenderWy] = self.virtualGridToWorldPos(offender.virtualGridX, offender.virtualGridY); - const bulletWx = offenderWx + xfac * meleeBullet.hitboxOffset; - const bulletWy = offenderWy + 0.5 * meleeBullet.hitboxSize.y; - const [bulletCx, bulletCy] = self.worldToPolygonColliderAnchorPos(bulletWx, bulletWy, meleeBullet.hitboxSize.x * 0.5, meleeBullet.hitboxSize.y * 0.5), - pts = [[0, 0], [meleeBullet.hitboxSize.x, 0], [meleeBullet.hitboxSize.x, meleeBullet.hitboxSize.y], [0, meleeBullet.hitboxSize.y]]; - const newBulletCollider = collisionSys.createPolygon(bulletCx, bulletCy, pts); - newBulletCollider.data = meleeBullet; - collisionSysMap.set(collisionBulletIndex, newBulletCollider); - bulletColliders.set(collisionBulletIndex, newBulletCollider); - // console.log(`A meleeBullet is added to collisionSys at currRenderFrame.id=${currRenderFrame.id} as start-up frames ended and active frame is not yet ended: ${JSON.stringify(meleeBullet)}`); - } - } - - collisionSys.update(); - const result1 = collisionSys.createResult(); // Can I reuse a "self.collisionSysResult" object throughout the whole battle? - - bulletColliders.forEach((bulletCollider, collisionBulletIndex) => { - const potentials = bulletCollider.potentials(); - const offender = currRenderFrame.players[bulletCollider.data.offenderPlayerId]; - let shouldRemove = false; - for (const potential of potentials) { - if (null != potential.data && potential.data.joinIndex == bulletCollider.data.offenderJoinIndex) continue; - if (!bulletCollider.collides(potential, result1)) continue; - if (null != potential.data && null !== potential.data.joinIndex) { - const joinIndex = potential.data.joinIndex; - let xfac = 1; - if (0 > offender.dirX) { - xfac = -1; - } - bulletPushbacks[joinIndex - 1][0] += xfac * bulletCollider.data.pushback; // Only for straight punch, there's no y-pushback - bulletPushbacks[joinIndex - 1][1] += 0; - const thatAckedPlayerInCurFrame = currRenderFrame.players[potential.data.id]; - const thatAckedPlayerInNextFrame = nextRenderFramePlayers[potential.data.id]; - thatAckedPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atked1[0]; - if (thatAckedPlayerInCurFrame.inAir) { - thatAckedPlayerInNextFrame.characterState = window.toInAirConjugate(thatAckedPlayerInNextFrame.characterState); - } - const oldFramesToRecover = thatAckedPlayerInNextFrame.framesToRecover; - thatAckedPlayerInNextFrame.framesToRecover = (oldFramesToRecover > bulletCollider.data.hitStunFrames ? oldFramesToRecover : bulletCollider.data.hitStunFrames); // In case the hit player is already stun, we extend it - } - shouldRemove = true; - } - if (shouldRemove) { - removedBulletsAtCurrFrame.add(collisionBulletIndex); - } - }); - - // [WARNING] Remove bullets from collisionSys ANYWAY for the convenience of rollback - for (let k in currRenderFrame.meleeBullets) { - const meleeBullet = currRenderFrame.meleeBullets[k]; - 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; - nextRenderFrameMeleeBullets.push(meleeBullet); - } - - // Process player inputs - if (null != delayedInputFrame) { - const delayedInputFrameForPrevRenderFrame = self.getCachedInputFrameDownsyncWithPrediction(self._convertToInputFrameId(currRenderFrame.id - 1, self.inputDelayFrames)); - const inputList = delayedInputFrame.inputList; - for (let j in self.playerRichInfoArr) { - const joinIndex = parseInt(j) + 1; - 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 currPlayerDownsync = currRenderFrame.players[playerId]; - const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; - if (0 < thatPlayerInNextFrame.framesToRecover) { - // No need to process inputs for this player, but there might be bullet pushbacks on this player - playerCollider.x += bulletPushbacks[joinIndex - 1][0]; - playerCollider.y += bulletPushbacks[joinIndex - 1][1]; - if (0 != bulletPushbacks[joinIndex - 1][0] || 0 != bulletPushbacks[joinIndex - 1][1]) { - console.log(`playerId=${playerId}, joinIndex=${joinIndex} is pushbacked back by ${bulletPushbacks[joinIndex - 1]} by bullet impacts, now its framesToRecover is ${thatPlayerInNextFrame.framesToRecover}`); - } - continue; - } - - const decodedInput = self.ctrl.decodeInput(inputList[joinIndex - 1]); - const prevDecodedInput = (null == delayedInputFrameForPrevRenderFrame ? null : self.ctrl.decodeInput(delayedInputFrameForPrevRenderFrame.inputList[joinIndex - 1])); - const prevBtnALevel = (null == prevDecodedInput ? 0 : prevDecodedInput.btnALevel); - const prevBtnBLevel = (null == prevDecodedInput ? 0 : prevDecodedInput.btnBLevel); - /* - [WARNING] Player input alone WOULD NOT take "characterState" into any "ATK_CHARACTER_STATE_IN_AIR_SET", only after the calculation of "effPushbacks" do we know exactly whether or not a player is "inAir", the finalize the transition of "thatPlayerInNextFrame.characterState". - */ - if (1 == decodedInput.btnBLevel && 0 == prevBtnBLevel) { - const characStateAlreadyInAir = window.ATK_CHARACTER_STATE_IN_AIR_SET.has(thatPlayerInNextFrame.characterState); - const characStateIsInterruptWaivable = window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(thatPlayerInNextFrame.characterState); - if ( - !characStateAlreadyInAir - && - characStateIsInterruptWaivable - ) { - thatPlayerInNextFrame.velY = self.jumpingInitVelY; - jumpTriggered[joinIndex - 1] = true; - console.log(`playerId=${playerId}, joinIndex=${joinIndex} triggered a rising-edge of btnB at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}, nextVelY=${thatPlayerInNextFrame.velY}, characStateAlreadyInAir=${characStateAlreadyInAir}, characStateIsInterruptWaivable=${characStateIsInterruptWaivable}`); - } - } - - 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}`); - if (self.bulletTriggerEnabled) { - const punchSkillId = 1; - const punch = window.pb.protos.MeleeBullet.create(self.meleeSkillConfig[punchSkillId]); - thatPlayerInNextFrame.framesToRecover = punch.recoveryFrames; - punch.battleLocalId = self.bulletBattleLocalIdCounter++; - punch.offenderJoinIndex = joinIndex; - punch.offenderPlayerId = playerId; - punch.originatedRenderFrameId = currRenderFrame.id; - nextRenderFrameMeleeBullets.push(punch); - // console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}: ${self._stringifyRecentInputCache(true)}`); - // console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); - - thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Atk1[0]; - if (false == currPlayerDownsync.inAir) { - thatPlayerInNextFrame.velX = 0; // prohibits simultaneous movement with Atk1 on the ground - } - } - } 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 bullet trigger, process joystick movement inputs (except for jumping). - if (0 != decodedInput.dx || 0 != decodedInput.dy) { - // Update directions and thus would eventually update moving animation accordingly - thatPlayerInNextFrame.dirX = decodedInput.dx; - thatPlayerInNextFrame.dirY = decodedInput.dy; - thatPlayerInNextFrame.velX = decodedInput.dx * currPlayerDownsync.speed; - thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Walking[0]; - } else { - thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0]; - thatPlayerInNextFrame.velX = 0; - } - } - if (currPlayerDownsync.inAir) { - thatPlayerInNextFrame.characterState = window.toInAirConjugate(thatPlayerInNextFrame.characterState); - } - } - } - - 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; - const playerId = self.playerRichInfoArr[j].id; - const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; - const playerCollider = collisionSysMap.get(collisionPlayerIndex); - const potentials = playerCollider.potentials(); - const currPlayerDownsync = currRenderFrame.players[playerId]; - const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; - let fallStopping = false; - let [snappedIntoPlatformEx, snappedIntoPlatformEy] = [null, null]; - for (const potential of potentials) { - // Test if the player collides with the wall - if (!playerCollider.collides(potential, result2)) continue; - // Push the player out of the wall - let [pushbackX, pushbackY] = [result2.overlap * result2.overlap_x, result2.overlap * result2.overlap_y]; - if (null == potential.data) { - // "null == potential.data" implies a barrier - const normAlignmentWithGravity = (result2.overlap_x * 0 + result2.overlap_y * (-1.0)); - const flatEnough = (self.snapIntoPlatformThreshold < normAlignmentWithGravity); // prevents false snapping on the lateral sides - const remainsNotInAir = (!currPlayerDownsync.inAir && flatEnough); - const localFallStopping = (currPlayerDownsync.inAir && flatEnough); - if (remainsNotInAir || localFallStopping) { - fallStopping |= localFallStopping; - [pushbackX, pushbackY] = [(result2.overlap - self.snapIntoPlatformOverlap) * result2.overlap_x, (result2.overlap - self.snapIntoPlatformOverlap) * result2.overlap_y] - // [overlay_x, overlap_y] is the unit vector that points into the platform; FIXME: Should only assign to [snappedIntoPlatformEx, snappedIntoPlatformEy] at most once! - snappedIntoPlatformEx = -result2.overlap_y; - snappedIntoPlatformEy = result2.overlap_x; - if (snappedIntoPlatformEx * currPlayerDownsync.dirX + snappedIntoPlatformEy * currPlayerDownsync.dirY) { - [snappedIntoPlatformEx, snappedIntoPlatformEy] = [-snappedIntoPlatformEx, -snappedIntoPlatformEy]; - } - } - } - // What if we're on the edge of 2 barriers? Would adding up make an unexpected bounce? - effPushbacks[joinIndex - 1][0] += pushbackX; - effPushbacks[joinIndex - 1][1] += pushbackY; - } - if (false == jumpTriggered[joinIndex - 1] && null != snappedIntoPlatformEx && null != snappedIntoPlatformEy) { - thatPlayerInNextFrame.inAir = false; - if (fallStopping) { - thatPlayerInNextFrame.velY = 0; - thatPlayerInNextFrame.velX = 0; - thatPlayerInNextFrame.characterState = window.ATK_CHARACTER_STATE.Idle1[0]; - thatPlayerInNextFrame.framesToRecover = 0; - } - const dotProd = thatPlayerInNextFrame.velX * snappedIntoPlatformEx + thatPlayerInNextFrame.velY * snappedIntoPlatformEy; - [thatPlayerInNextFrame.velX, thatPlayerInNextFrame.velY] = [dotProd * snappedIntoPlatformEx, dotProd * snappedIntoPlatformEy]; - } - } - - // Get players out of stuck barriers if there's any - for (let j in self.playerRichInfoArr) { - const joinIndex = parseInt(j) + 1; - const playerId = self.playerRichInfoArr[j].id; - const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; - const playerCollider = collisionSysMap.get(collisionPlayerIndex); - const thatPlayerInNextFrame = nextRenderFramePlayers[playerId]; - [thatPlayerInNextFrame.virtualGridX, thatPlayerInNextFrame.virtualGridY] = self.polygonColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[joinIndex - 1][0], playerCollider.y - effPushbacks[joinIndex - 1][1], self.playerRichInfoArr[j].colliderRadius, self.playerRichInfoArr[j].colliderRadius); - } - - return window.pb.protos.RoomDownsyncFrame.create({ - id: currRenderFrame.id + 1, - players: nextRenderFramePlayers, - meleeBullets: nextRenderFrameMeleeBullets, - }); - }, - - applyRoomDownsyncFrameDynamics(rdf, prevRdf) { - const self = this; - OnlineMap.prototype.applyRoomDownsyncFrameDynamics.call(self, rdf, prevRdf); - if (self.showCriticalCoordinateLabels) { - let g = self.g; - g.clear(); - - for (let k in self.collisionSys._bvh._bodies) { - const body = self.collisionSys._bvh._bodies[k]; - if (!body._polygon) continue; - if (null != body.data && null != body.data.joinIndex) { - // character - if (1 == body.data.joinIndex) { - g.strokeColor = cc.Color.BLUE; - } else { - g.strokeColor = cc.Color.RED; - } - } else { - // barrier - g.strokeColor = cc.Color.WHITE; - } - g.moveTo(body.x, body.y); - const cnt = body._coords.length; - for (let j = 0; j < cnt; j += 2) { - const x = body._coords[j], - y = body._coords[j + 1]; - g.lineTo(x, y); - } - g.lineTo(body.x, body.y); - g.stroke(); - } - // For convenience of recovery upon reconnection, active bullets are always created & immediately removed from "collisionSys" within "applyInputFrameDownsyncDynamicsOnSingleRenderFrame" - - for (let k in rdf.meleeBullets) { - const meleeBullet = rdf.meleeBullets[k]; - if ( - meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames <= rdf.id - && - meleeBullet.originatedRenderFrameId + meleeBullet.startupFrames + meleeBullet.activeFrames > rdf.id - ) { - const offender = rdf.players[meleeBullet.offenderPlayerId]; - if (1 == offender.joinIndex) { - g.strokeColor = cc.Color.BLUE; - } else { - g.strokeColor = cc.Color.RED; - } - - let xfac = 1; // By now, straight Punch offset doesn't respect "y-axis" - if (0 > offender.dirX) { - xfac = -1; - } - const [offenderWx, offenderWy] = self.virtualGridToWorldPos(offender.virtualGridX, offender.virtualGridY); - const bulletWx = offenderWx + xfac * meleeBullet.hitboxOffset; - const bulletWy = offenderWy + 0.5 * meleeBullet.hitboxSize.y; - const [bulletCx, bulletCy] = self.worldToPolygonColliderAnchorPos(bulletWx, bulletWy, meleeBullet.hitboxSize.x * 0.5, meleeBullet.hitboxSize.y * 0.5), - pts = [[0, 0], [meleeBullet.hitboxSize.x, 0], [meleeBullet.hitboxSize.x, meleeBullet.hitboxSize.y], [0, meleeBullet.hitboxSize.y]]; - - g.moveTo(bulletCx, bulletCy); - for (let j = 0; j < pts.length; j += 1) { - g.lineTo(pts[j][0] + bulletCx, pts[j][1] + bulletCy); - } - g.lineTo(bulletCx, bulletCy); - g.stroke(); - } } } },