diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index e5e2b6f..66b16da 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -1401,8 +1401,7 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF } offenderWx, offenderWy := VirtualGridToWorldPos(offender.VirtualGridX, offender.VirtualGridY, pR.VirtualGridToWorldRatio) bulletWx, bulletWy := offenderWx+xfac*meleeBullet.HitboxOffset, offenderWy - newBulletCollider := GenerateRectCollider(bulletWx, bulletWy, meleeBullet.HitboxSize.X, meleeBullet.HitboxSize.Y, topPadding, bottomPadding, leftPadding, rightPadding, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "MeleeBullet") - newBulletCollider.Data = meleeBullet + newBulletCollider := GenerateRectCollider(bulletWx, bulletWy, meleeBullet.HitboxSize.X, meleeBullet.HitboxSize.Y, topPadding, bottomPadding, leftPadding, rightPadding, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, meleeBullet, "MeleeBullet") pR.Space.Add(newBulletCollider) collisionSysMap[collisionBulletIndex] = newBulletCollider bulletColliders[collisionBulletIndex] = newBulletCollider @@ -1625,8 +1624,7 @@ func (pR *Room) refreshColliders(spaceW, spaceH int32) { for _, player := range pR.Players { wx, wy := VirtualGridToWorldPos(player.VirtualGridX, player.VirtualGridY, pR.VirtualGridToWorldRatio) colliderWidth, colliderHeight := player.ColliderRadius*2, player.ColliderRadius*4 - playerCollider := GenerateRectCollider(wx, wy, colliderWidth, colliderHeight, topPadding, bottomPadding, leftPadding, rightPadding, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Player") // the coords of all barrier boundaries are multiples of tileWidth(i.e. 16), by adding snapping y-padding when "landedOnGravityPushback" all "playerCollider.Y" would be a multiple of 1.0 - playerCollider.Data = player + playerCollider := GenerateRectCollider(wx, wy, colliderWidth, colliderHeight, topPadding, bottomPadding, leftPadding, rightPadding, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, player, "Player") // the coords of all barrier boundaries are multiples of tileWidth(i.e. 16), by adding snapping y-padding when "landedOnGravityPushback" all "playerCollider.Y" would be a multiple of 1.0 pR.Space.Add(playerCollider) // Keep track of the collider in "pR.CollisionSysMap" joinIndex := player.JoinIndex @@ -1637,8 +1635,7 @@ func (pR *Room) refreshColliders(spaceW, spaceH int32) { for _, barrier := range pR.Barriers { boundaryUnaligned := barrier.Boundary - barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, "Barrier") - barrierCollider.Data = barrier + barrierCollider := GenerateConvexPolygonCollider(boundaryUnaligned, pR.collisionSpaceOffsetX, pR.collisionSpaceOffsetY, barrier, "Barrier") pR.Space.Add(barrierCollider) } } diff --git a/collider_visualizer/worldColliderDisplay.go b/collider_visualizer/worldColliderDisplay.go index cf1dc4c..1ac60fb 100644 --- a/collider_visualizer/worldColliderDisplay.go +++ b/collider_visualizer/worldColliderDisplay.go @@ -45,7 +45,7 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi topPadding, bottomPadding, leftPadding, rightPadding := snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap for i, playerPos := range playerPosList.Eles { colliderWidth, colliderHeight := playerColliderRadius*2, playerColliderRadius*4 - playerCollider := GenerateRectCollider(playerPos.X, playerPos.Y, colliderWidth, colliderHeight, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, "Player") // [WARNING] Deliberately not using a circle because "resolv v0.5.1" doesn't yet align circle center with space cell center, regardless of the "specified within-object offset" + playerCollider := GenerateRectCollider(playerPos.X, playerPos.Y, colliderWidth, colliderHeight, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, nil, "Player") // [WARNING] Deliberately not using a circle because "resolv v0.5.1" doesn't yet align circle center with space cell center, regardless of the "specified within-object offset" Logger.Info(fmt.Sprintf("Player Collider#%d: player world pos=(%.2f, %.2f), shape=%v", i, playerPos.X, playerPos.Y, ConvexPolygonStr(playerCollider.Shape.(*resolv.ConvexPolygon)))) playerColliders[i] = playerCollider space.Add(playerCollider) @@ -53,7 +53,7 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi barrierLocalId := 0 for _, barrierUnaligned := range barrierList.Eles { - barrierCollider := GenerateConvexPolygonCollider(barrierUnaligned, spaceOffsetX, spaceOffsetY, "Barrier") + barrierCollider := GenerateConvexPolygonCollider(barrierUnaligned, spaceOffsetX, spaceOffsetY, nil, "Barrier") Logger.Debug(fmt.Sprintf("Added barrier: shape=%v", ConvexPolygonStr(barrierCollider.Shape.(*resolv.ConvexPolygon)))) space.Add(barrierCollider) barrierLocalId++ @@ -122,7 +122,7 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi xfac := float64(1.0) offenderWx, offenderWy := playerPosList.Eles[0].X, playerPosList.Eles[0].Y bulletWx, bulletWy := offenderWx+xfac*meleeBullet.HitboxOffset, offenderWy - newBulletCollider := GenerateRectCollider(bulletWx, bulletWy, meleeBullet.HitboxSize.X, meleeBullet.HitboxSize.Y, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, "MeleeBullet") + newBulletCollider := GenerateRectCollider(bulletWx, bulletWy, meleeBullet.HitboxSize.X, meleeBullet.HitboxSize.Y, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, nil, "MeleeBullet") space.Add(newBulletCollider) bulletShape := newBulletCollider.Shape.(*resolv.ConvexPolygon) Logger.Warn(fmt.Sprintf("bullet ->: Added bullet collider to space: a=%v", ConvexPolygonStr(bulletShape))) @@ -145,7 +145,7 @@ func NewWorldColliderDisplay(game *Game, stageDiscreteW, stageDiscreteH, stageTi offenderWx, offenderWy := playerPosList.Eles[1].X, playerPosList.Eles[1].Y bulletWx, bulletWy := offenderWx+xfac*meleeBullet.HitboxOffset, offenderWy - newBulletCollider := GenerateRectCollider(bulletWx, bulletWy, meleeBullet.HitboxSize.X, meleeBullet.HitboxSize.Y, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, "MeleeBullet") + newBulletCollider := GenerateRectCollider(bulletWx, bulletWy, meleeBullet.HitboxSize.X, meleeBullet.HitboxSize.Y, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, nil, "MeleeBullet") space.Add(newBulletCollider) bulletShape := newBulletCollider.Shape.(*resolv.ConvexPolygon) Logger.Warn(fmt.Sprintf("bullet <-: Added bullet collider to space: a=%v", ConvexPolygonStr(bulletShape))) diff --git a/dnmshared/resolv_helper.go b/dnmshared/resolv_helper.go index cb91238..1aff986 100644 --- a/dnmshared/resolv_helper.go +++ b/dnmshared/resolv_helper.go @@ -22,19 +22,20 @@ func RectCenterStr(body *resolv.Object, halfBoundingW, halfBoundingH, topPadding return fmt.Sprintf("{%.2f, %.2f}", body.X+leftPadding+halfBoundingW-spaceOffsetX, body.Y+bottomPadding+halfBoundingH-spaceOffsetY) } -func GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object { +func GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *resolv.Object { blX, blY := WorldToPolygonColliderBLPos(wx, wy, w*0.5, h*0.5, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY) - return generateRectColliderInCollisionSpace(blX, blY, leftPadding+w+rightPadding, bottomPadding+h+topPadding, tag) + return generateRectColliderInCollisionSpace(blX, blY, leftPadding+w+rightPadding, bottomPadding+h+topPadding, data, tag) } -func generateRectColliderInCollisionSpace(blX, blY, w, h float64, tag string) *resolv.Object { +func generateRectColliderInCollisionSpace(blX, blY, w, h float64, data interface{}, tag string) *resolv.Object { collider := resolv.NewObject(blX, blY, w, h, tag) // Unlike its frontend counter part, the position of a "resolv.Object" must be specified by "bottom-left point" because "w" and "h" must be positive, see "resolv.Object.BoundsToSpace" for details shape := resolv.NewRectangle(0, 0, w, h) collider.SetShape(shape) + collider.Data = data return collider } -func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, tag string) *resolv.Object { +func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *resolv.Object { aligned := AlignPolygon2DToBoundingBox(unalignedSrc) var w, h float64 = 0, 0 @@ -60,6 +61,7 @@ func GenerateConvexPolygonCollider(unalignedSrc *Polygon2D, spaceOffsetX, spaceO collider := resolv.NewObject(aligned.Anchor.X+spaceOffsetX, aligned.Anchor.Y+spaceOffsetY, w, h, tag) collider.SetShape(shape) + collider.Data = data return collider } diff --git a/dnmshared/ringbuf.go b/dnmshared/ringbuf.go index dfddab9..61ea25f 100644 --- a/dnmshared/ringbuf.go +++ b/dnmshared/ringbuf.go @@ -1,5 +1,11 @@ package dnmshared +const ( + RING_BUFF_CONSECUTIVE_SET = int32(0) + RING_BUFF_NON_CONSECUTIVE_SET = int32(1) + RING_BUFF_FAILED_TO_SET = int32(2) +) + type RingBuffer struct { Ed int32 // write index, open index St int32 // read index, closed index @@ -48,15 +54,15 @@ func (rb *RingBuffer) Pop() interface{} { return pItem } -func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} { - if 0 == rb.Cnt { - return nil +func (rb *RingBuffer) GetArrIdxByOffset(offsetFromSt int32) int32 { + if 0 == rb.Cnt || 0 > offsetFromSt { + return -1 } arrIdx := rb.St + offsetFromSt if rb.St < rb.Ed { // case#1: 0...st...ed...N-1 if rb.St <= arrIdx && arrIdx < rb.Ed { - return rb.Eles[arrIdx] + return arrIdx } } else { // if rb.St >= rb.Ed @@ -65,11 +71,19 @@ func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} { arrIdx -= rb.N } if arrIdx >= rb.St || arrIdx < rb.Ed { - return rb.Eles[arrIdx] + return arrIdx } } - return nil + return -1 +} + +func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} { + arrIdx := rb.GetArrIdxByOffset(offsetFromSt) + if -1 == arrIdx { + return nil + } + return rb.Eles[arrIdx] } func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} { @@ -78,3 +92,33 @@ func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} { } return rb.GetByOffset(frameId - rb.StFrameId) } + +// [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly. +func (rb *RingBuffer) SetByFrameId(pItem interface{}, frameId int32) (int32, int32, int32) { + oldStFrameId, oldEdFrameId := rb.StFrameId, rb.EdFrameId + if frameId < oldStFrameId { + return RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId + } + // By now "rb.StFrameId <= frameId" + if oldEdFrameId > frameId { + arrIdx := rb.GetArrIdxByOffset(frameId - rb.StFrameId) + if -1 != arrIdx { + rb.Eles[arrIdx] = pItem + return RING_BUFF_CONSECUTIVE_SET, oldStFrameId, oldEdFrameId + } + } + + // By now "rb.EdFrameId <= frameId" + ret := RING_BUFF_CONSECUTIVE_SET + if oldEdFrameId < frameId { + rb.St, rb.Ed = 0, 0 + rb.StFrameId, rb.EdFrameId = frameId, frameId + rb.Cnt = 0 + ret = RING_BUFF_NON_CONSECUTIVE_SET + } + + // By now "rb.EdFrameId == frameId" + rb.Put(pItem) + + return ret, oldStFrameId, oldEdFrameId +} diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index 31ccba3..740ec4a 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 209.73151519075364, + 215.95961841836203, 0, 0, 0, diff --git a/frontend/assets/scenes/offline_map_2.fire b/frontend/assets/scenes/offline_map_2.fire new file mode 100644 index 0000000..0a155eb --- /dev/null +++ b/frontend/assets/scenes/offline_map_2.fire @@ -0,0 +1,1361 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "", + "_objFlags": 0, + "_native": "", + "scene": { + "__id__": 1 + } + }, + { + "__type__": "cc.Scene", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": false, + "_components": [], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 0 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_is3DNode": true, + "_groupIndex": 0, + "groupIndex": 0, + "autoReleaseAssets": false, + "_id": "8491a86c-bec9-4813-968a-128ca01639e0" + }, + { + "__type__": "cc.Node", + "_name": "Canvas", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 9 + } + ], + "_active": true, + "_components": [ + { + "__id__": 27 + }, + { + "__id__": 28 + }, + { + "__id__": 29 + }, + { + "__id__": 30 + }, + { + "__id__": 31 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1024, + "height": 1920 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 512, + 960, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "daDUxCjRFEHak7fx7LvgSJ" + }, + { + "__type__": "cc.Node", + "_name": "Map", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + }, + { + "__id__": 5 + }, + { + "__id__": 6 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 2048, + "height": 2048 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1.5, + 1.5, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "79f0Sv9OVGwbY3M3efHnGf" + }, + { + "__type__": "cc.TiledMap", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_tmxFile": null, + "_id": "c8MqKDLJdKz7VhPwMjScDw" + }, + { + "__type__": "09e1b/tEy5K2qaPIpqHDbae", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "BGMEffect": { + "__uuid__": "64a79efa-97de-4cb5-b2a9-01500c60573a" + }, + "crashedByTrapBullet": { + "__uuid__": "1d604e42-8cee-466f-884d-e74cae21ce3b" + }, + "highScoreTreasurePicked": { + "__uuid__": "0164d22c-d965-461f-867e-b30e2d56cc5c" + }, + "treasurePicked": { + "__uuid__": "7704b97e-6367-420c-b7af-d0750a2bbb30" + }, + "countDown10SecToEnd": { + "__uuid__": "261d1d7d-a5cc-4cb7-a737-194427055fd4" + }, + "mapNode": { + "__id__": 3 + }, + "_id": "3crA1nz5xPSLAnCSLQIPOq" + }, + { + "__type__": "47d7dy4S4lB2pxqJJlGOoai", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "canvasNode": { + "__id__": 2 + }, + "controlledCharacterPrefab": { + "__uuid__": "59bff7a2-23e1-4d69-bce7-afb37eae196a" + }, + "joystickInputControllerNode": { + "__id__": 7 + }, + "confirmLogoutPrefab": null, + "simplePressToGoDialogPrefab": null, + "boundRoomIdLabel": null, + "countdownLabel": null, + "resultPanelPrefab": null, + "gameRulePrefab": null, + "findingPlayerPrefab": null, + "countdownToBeginGamePrefab": null, + "playersInfoPrefab": null, + "forceBigEndianFloatingNumDecoding": false, + "renderFrameIdLagTolerance": 4, + "jigglingEps1D": 0.001, + "bulletTriggerEnabled": true, + "closeOnForcedtoResyncNotSelf": true, + "_id": "4b+kZ46VhC0LCBixXEK2dk" + }, + { + "__type__": "cc.Node", + "_name": "JoystickContainer", + "_objFlags": 0, + "_parent": { + "__id__": 8 + }, + "_children": [ + { + "__id__": 21 + } + ], + "_active": true, + "_components": [ + { + "__id__": 26 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 1280, + "height": 640 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + -500, + 0, + 0, + 0, + 0, + 1, + 0.66667, + 0.66667, + 0.66667 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "81iBXkC0lFt5FFUUD0k3xE" + }, + { + "__type__": "cc.Node", + "_name": "WidgetsAboveAll", + "_objFlags": 0, + "_parent": { + "__id__": 9 + }, + "_children": [ + { + "__id__": 7 + }, + { + "__id__": 11 + }, + { + "__id__": 13 + }, + { + "__id__": 17 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 0 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "c6fPdAUURDX69j0zTeIFZv" + }, + { + "__type__": "cc.Node", + "_name": "Main Camera", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 8 + } + ], + "_active": true, + "_components": [ + { + "__id__": 10 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 0 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 209.64964554467088, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "76BOk0enxN+b20dtIJ3uk2" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 9 + }, + "_enabled": true, + "_cullingMask": 4294967295, + "_clearFlags": 7, + "_backgroundColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 255 + }, + "_depth": -1, + "_zoomRatio": 1.5, + "_targetTexture": null, + "_fov": 60, + "_orthoSize": 10, + "_nearClip": 1, + "_farClip": 4096, + "_ortho": true, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_renderStages": 1, + "_alignWithScreen": true, + "_id": "50qiPTLS9NhbPa+JZU0jOP" + }, + { + "__type__": "cc.Node", + "_name": "CountdownSeconds", + "_objFlags": 0, + "_parent": { + "__id__": 8 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 12 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 0, + "height": 88.2 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 591, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "f6HMd9ebFPppe/Lu0Ry/qk" + }, + { + "__type__": "cc.Label", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 11 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_useOriginalSize": false, + "_string": "", + "_N$string": "", + "_fontSize": 70, + "_lineHeight": 70, + "_enableWrapText": true, + "_N$file": null, + "_isSystemFontUsed": true, + "_spacingX": 0, + "_batchAsBitmap": false, + "_N$horizontalAlign": 1, + "_N$verticalAlign": 1, + "_N$fontFamily": "Arial", + "_N$overflow": 0, + "_N$cacheMode": 0, + "_id": "dfxSFl+shLcY+0v45FJtGo" + }, + { + "__type__": "cc.Node", + "_name": "BtnA", + "_objFlags": 0, + "_parent": { + "__id__": 8 + }, + "_children": [ + { + "__id__": 14 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 200, + "height": 200 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 143.689, + -584.849, + 0, + 0, + 0, + 0, + 1, + 0.66667, + 0.66667, + 0.66667 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "a5pbciqqBEiYlyy7SLwP4V" + }, + { + "__type__": "cc.Node", + "_name": "Background", + "_objFlags": 0, + "_parent": { + "__id__": 13 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 15 + }, + { + "__id__": 16 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 200, + "height": 200 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "ffojLoj41JCpizkLkManxw" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 14 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "350fd890-3d28-4e53-9dfa-1bf00d857737" + }, + "_type": 1, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": { + "__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4" + }, + "_id": "21Oz9QwBZPALzRoJujKVPG" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 14 + }, + "_enabled": true, + "alignMode": 0, + "_target": null, + "_alignFlags": 45, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_verticalCenter": 0, + "_horizontalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 100, + "_originalHeight": 40, + "_id": "beUmgK1D1M07I7kTL4NFLK" + }, + { + "__type__": "cc.Node", + "_name": "BtnB", + "_objFlags": 0, + "_parent": { + "__id__": 8 + }, + "_children": [ + { + "__id__": 18 + } + ], + "_active": true, + "_components": [], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 200, + "height": 200 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 370.368, + -424.647, + 0, + 0, + 0, + 0, + 1, + 0.66667, + 0.66667, + 0.66667 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "9eZHUAtItPMKp7OrItdaWA" + }, + { + "__type__": "cc.Node", + "_name": "Background", + "_objFlags": 0, + "_parent": { + "__id__": 17 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 19 + }, + { + "__id__": 20 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 200, + "height": 200 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + -1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "e2JXxje+hAC7LjZqzgq8RF" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 18 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "a2170e4c-df31-41ef-be73-f4f605e75821" + }, + "_type": 1, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": { + "__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4" + }, + "_id": "54DvUzQvpKMrmsuRaQRSxf" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 18 + }, + "_enabled": true, + "alignMode": 0, + "_target": null, + "_alignFlags": 45, + "_left": 0, + "_right": 0, + "_top": 0, + "_bottom": 0, + "_verticalCenter": 0, + "_horizontalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 100, + "_originalHeight": 40, + "_id": "5fVLGAIhROj6UDGotnneup" + }, + { + "__type__": "cc.Node", + "_name": "JoystickBG", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [ + { + "__id__": 22 + } + ], + "_active": true, + "_components": [ + { + "__id__": 24 + }, + { + "__id__": 25 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 400, + "height": 400 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + -380, + 0, + 0, + 0, + 0, + 0, + 1, + 1, + 1, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "88u3wQvvdO8pbrNWhs3ifP" + }, + { + "__type__": "cc.Node", + "_name": "Joystick", + "_objFlags": 0, + "_parent": { + "__id__": 21 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 23 + } + ], + "_prefab": null, + "_opacity": 255, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_contentSize": { + "__type__": "cc.Size", + "width": 150, + "height": 150 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_trs": { + "__type__": "TypedArray", + "ctor": "Float64Array", + "array": [ + 0, + 0, + 0, + 0, + 0, + 0, + 1, + 0.8, + 0.8, + 1 + ] + }, + "_eulerAngles": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_skewX": 0, + "_skewY": 0, + "_is3DNode": false, + "_groupIndex": 0, + "groupIndex": 0, + "_id": "3eybpdW/JK3aDeXxdE86VD" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 22 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "7d4baacd-294c-4a5d-9cd6-5d36e4394c9e" + }, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": { + "__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4" + }, + "_id": "7dr8DOX01K7YFqWlRy1ATp" + }, + { + "__type__": "cc.Sprite", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 21 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "eca5d2f2-8ef6-41c2-bbe6-f9c79d09c432" + } + ], + "_srcBlendFactor": 770, + "_dstBlendFactor": 771, + "_spriteFrame": { + "__uuid__": "447f7cfe-e678-4424-be03-0afdab8659de" + }, + "_type": 0, + "_sizeMode": 0, + "_fillType": 0, + "_fillCenter": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_fillStart": 0, + "_fillRange": 0, + "_isTrimmedMode": true, + "_atlas": { + "__uuid__": "030d9286-e8a2-40cf-98f8-baf713f0b8c4" + }, + "_id": "b28Bh9ZcpM+7K3Bd3bmNf0" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 21 + }, + "_enabled": true, + "alignMode": 0, + "_target": null, + "_alignFlags": 0, + "_left": 40, + "_right": 0, + "_top": 0, + "_bottom": 10, + "_verticalCenter": 0, + "_horizontalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 0, + "_originalHeight": 0, + "_id": "c0cEsj4LpMcZZEldELidxy" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 7 + }, + "_enabled": true, + "alignMode": 0, + "_target": null, + "_alignFlags": 0, + "_left": 278, + "_right": 480.0000000000002, + "_top": 544, + "_bottom": 0, + "_verticalCenter": 0, + "_horizontalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 480, + "_originalHeight": 0, + "_id": "2cxYjEIwNO6rUtXX4WcfnV" + }, + { + "__type__": "cc.Canvas", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "_designResolution": { + "__type__": "cc.Size", + "width": 1024, + "height": 1920 + }, + "_fitWidth": true, + "_fitHeight": false, + "_id": "94aSq7GcJJZ7A6IzMerm1J" + }, + { + "__type__": "8ac08Cb+Y1M/6ZsO9niGOzW", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "map": { + "__id__": 3 + }, + "_id": "84o2sgpN1NRqlN9x7mSzBj" + }, + { + "__type__": "78830/HTiVJoaf8n504g/J4", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "mapNode": { + "__id__": 3 + }, + "speed": 5000, + "_id": "76ImpM7XtPSbiLHDXdsJa+" + }, + { + "__type__": "cc.Widget", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "alignMode": 0, + "_target": null, + "_alignFlags": 0, + "_left": 6240, + "_right": -4000, + "_top": -8320, + "_bottom": 10880, + "_verticalCenter": 0, + "_horizontalCenter": 0, + "_isAbsLeft": true, + "_isAbsRight": true, + "_isAbsTop": true, + "_isAbsBottom": true, + "_isAbsHorizontalCenter": true, + "_isAbsVerticalCenter": true, + "_originalWidth": 960, + "_originalHeight": 640, + "_id": "a8lQ6mB8RMRajCXQCzw1kG" + }, + { + "__type__": "d34e3c4jd5NqYtg8ltL9QST", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "translationListenerNode": { + "__id__": 7 + }, + "zoomingListenerNode": { + "__id__": 3 + }, + "stickhead": { + "__id__": 22 + }, + "base": { + "__id__": 21 + }, + "joyStickEps": 0.1, + "magicLeanLowerBound": 0.414, + "magicLeanUpperBound": 2.414, + "linearScaleFacBase": 1, + "minScale": 1, + "maxScale": 2, + "maxMovingBufferLength": 1, + "zoomingScaleFacBase": 0.1, + "zoomingSpeedBase": 4, + "linearSpeedBase": 320, + "canvasNode": { + "__id__": 2 + }, + "mapNode": { + "__id__": 3 + }, + "linearMovingEps": 0.1, + "scaleByEps": 0.0375, + "btnA": { + "__id__": 13 + }, + "btnB": { + "__id__": 17 + }, + "_id": "e9oVYTr7ROlpp/IrNjBUmR" + } +] \ No newline at end of file diff --git a/frontend/assets/scenes/offline_map_2.fire.meta b/frontend/assets/scenes/offline_map_2.fire.meta new file mode 100644 index 0000000..85fb4b0 --- /dev/null +++ b/frontend/assets/scenes/offline_map_2.fire.meta @@ -0,0 +1,7 @@ +{ + "ver": "1.2.5", + "uuid": "8491a86c-bec9-4813-968a-128ca01639e0", + "asyncLoadAssets": false, + "autoReleaseAssets": false, + "subMetas": {} +} \ No newline at end of file diff --git a/frontend/assets/scripts/OfflineMapBackend.js b/frontend/assets/scripts/OfflineMapBackend.js new file mode 100644 index 0000000..72272a9 --- /dev/null +++ b/frontend/assets/scripts/OfflineMapBackend.js @@ -0,0 +1,238 @@ +const i18n = require('LanguageData'); +i18n.init(window.language); // languageID should be equal to the one we input in New Language ID input field + +const OnlineMap = require('./Map'); + +cc.Class({ + extends: OnlineMap, + + onDestroy() { + console.warn("+++++++ Map onDestroy()"); + }, + + onLoad() { + const self = this; + window.mapIns = self; + self.showCriticalCoordinateLabels = true; + + const mapNode = self.node; + const canvasNode = mapNode.parent; + + self.mainCameraNode = self.canvasNode.getChildByName("Main Camera"); + self.mainCamera = self.mainCameraNode.getComponent(cc.Camera); + for (let child of self.mainCameraNode.children) { + child.setScale(1 / self.mainCamera.zoomRatio); + } + self.widgetsAboveAllNode = self.mainCameraNode.getChildByName("WidgetsAboveAll"); + self.mainCameraNode.setPosition(cc.v2()); + + /** Init required prefab ended. */ + + self.inputDelayFrames = 8; + self.inputScaleFrames = 2; + self.inputFrameUpsyncDelayTolerance = 2; + + self.renderCacheSize = 1024; + self.serverFps = 60; + 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; + self.meleeSkillConfig = { + 1: { + // for offender + startupFrames: 10, + activeFrames: 20, + recoveryFrames: 34, // usually but not always "startupFrames+activeFrames", I hereby set it to be 1 frame more than the actual animation to avoid critical transition, i.e. when the animation is 1 frame from ending but "rdfPlayer.framesToRecover" is already counted 0 and the player triggers an other same attack, making an effective bullet trigger but no animation is played due to same animName is still playing + recoveryFramesOnBlock: 34, + recoveryFramesOnHit: 34, + moveforward: { + x: 0, + y: 0, + }, + hitboxOffset: 12.0, // should be about the radius of the PlayerCollider + hitboxSize: { + x: 23.0, + y: 32.0, + }, + + // for defender + hitStunFrames: 18, + blockStunFrames: 9, + pushback: 8.0, + releaseTriggerType: 1, // 1: rising-edge, 2: falling-edge + damage: 5 + } + }; + + /* + [WARNING] As when a character is standing on a barrier, if not carefully curated there MIGHT BE a bouncing sequence of "[(inAir -> dropIntoBarrier ->), (notInAir -> pushedOutOfBarrier ->)], [(inAir -> ..." + + Moreover, "snapIntoPlatformOverlap" should be small enough such that the walking "velX" or jumping initial "velY" can escape from it by 1 renderFrame (when jumping is triggered, the character is waived from snappig for 1 renderFrame). + */ + self.snapIntoPlatformOverlap = 0.1; + self.snapIntoPlatformThreshold = 0.5; // a platform must be "horizontal enough" for a character to "stand on" + self.jumpingInitVelY = 7 * self.worldToVirtualGridRatio; // unit: (virtual grid length/renderFrame) + [self.gravityX, self.gravityY] = [0, -0.5*self.worldToVirtualGridRatio]; // unit: (virtual grid length/renderFrame^2) + + const tiledMapIns = self.node.getComponent(cc.TiledMap); + + const fullPathOfTmxFile = cc.js.formatStr("map/%s/map", "dungeon"); + cc.loader.loadRes(fullPathOfTmxFile, cc.TiledMapAsset, (err, tmxAsset) => { + if (null != err) { + console.error(err); + return; + } + + tiledMapIns.tmxAsset = null; + mapNode.removeAllChildren(); + self._resetCurrentMatch(); + + if (self.showCriticalCoordinateLabels) { + const drawer = new cc.Node(); + drawer.setPosition(cc.v2(0, 0)) + safelyAddChild(self.node, drawer); + setLocalZOrder(drawer, 999); + const g = drawer.addComponent(cc.Graphics); + g.lineWidth = 2; + self.g = g; + } + + + tiledMapIns.tmxAsset = tmxAsset; + const newMapSize = tiledMapIns.getMapSize(); + const newTileSize = tiledMapIns.getTileSize(); + self.node.setContentSize(newMapSize.width * newTileSize.width, newMapSize.height * newTileSize.height); + self.node.setPosition(cc.v2(0, 0)); + + let barrierIdCounter = 0; + const boundaryObjs = tileCollisionManager.extractBoundaryObjects(self.node); + for (let boundaryObj of boundaryObjs.barriers) { + const x0 = boundaryObj.anchor.x, + y0 = boundaryObj.anchor.y; + + const newBarrier = self.collisionSys.createPolygon(x0, y0, Array.from(boundaryObj, p => { + return [p.x, p.y]; + })); + newBarrier.data = { + hardPushback: true + }; + + if (false && self.showCriticalCoordinateLabels) { + for (let i = 0; i < boundaryObj.length; ++i) { + const barrierVertLabelNode = new cc.Node(); + switch (i % 4) { + case 0: + barrierVertLabelNode.color = cc.Color.RED; + break; + case 1: + barrierVertLabelNode.color = cc.Color.GRAY; + break; + case 2: + barrierVertLabelNode.color = cc.Color.BLACK; + break; + default: + barrierVertLabelNode.color = cc.Color.MAGENTA; + break; + } + const wx = boundaryObj.anchor.x + boundaryObj[i].x, + wy = boundaryObj.anchor.y + boundaryObj[i].y; + barrierVertLabelNode.setPosition(cc.v2(wx, wy)); + const barrierVertLabel = barrierVertLabelNode.addComponent(cc.Label); + barrierVertLabel.fontSize = 12; + barrierVertLabel.lineHeight = barrierVertLabel.fontSize + 1; + barrierVertLabel.string = `(${wx.toFixed(1)}, ${wy.toFixed(1)})`; + safelyAddChild(self.node, barrierVertLabelNode); + setLocalZOrder(barrierVertLabelNode, 5); + + barrierVertLabelNode.active = true; + } + + } + // console.log("Created barrier: ", newBarrier); + ++barrierIdCounter; + const collisionBarrierIndex = (self.collisionBarrierIndexPrefix + barrierIdCounter); + self.collisionSysMap.set(collisionBarrierIndex, newBarrier); + } + + const startRdf = window.pb.protos.RoomDownsyncFrame.create({ + id: window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START, + players: { + 10: window.pb.protos.PlayerDownsync.create({ + id: 10, + joinIndex: 1, + virtualGridX: self.worldToVirtualGridPos(boundaryObjs.playerStartingPositions[0].x, boundaryObjs.playerStartingPositions[0].y)[0], + virtualGridY: self.worldToVirtualGridPos(boundaryObjs.playerStartingPositions[0].x, boundaryObjs.playerStartingPositions[0].y)[1], + speed: 1 * self.worldToVirtualGridRatio, + colliderRadius: 12, + characterState: window.ATK_CHARACTER_STATE.InAirIdle1[0], + framesToRecover: 0, + dirX: 0, + dirY: 0, + velX: 0, + velY: 0, + inAir: true, + }), + 11: window.pb.protos.PlayerDownsync.create({ + id: 11, + joinIndex: 2, + virtualGridX: self.worldToVirtualGridPos(boundaryObjs.playerStartingPositions[1].x, boundaryObjs.playerStartingPositions[1].y)[0], + virtualGridY: self.worldToVirtualGridPos(boundaryObjs.playerStartingPositions[1].x, boundaryObjs.playerStartingPositions[1].y)[1], + speed: 1 * self.worldToVirtualGridRatio, + colliderRadius: 12, + characterState: window.ATK_CHARACTER_STATE.InAirIdle1[0], + framesToRecover: 0, + dirX: 0, + dirY: 0, + velX: 0, + velY: 0, + inAir: true, + }), + } + }); + self.selfPlayerInfo = { + id: 11 + }; + self._initPlayerRichInfoDict(startRdf.players); + self.onRoomDownsyncFrame(startRdf); + + self.battleState = ALL_BATTLE_STATES.IN_BATTLE; + }); + + }, + + update(dt) { + const self = this; + if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) { + const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt; + 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; + } + try { + let st = performance.now(); + let prevSelfInput = null, + currSelfInput = null; + const noDelayInputFrameId = self._convertToInputFrameId(self.renderFrameId, 0); // It's important that "inputDelayFrames == 0" here + if (self.shouldGenerateInputFrameUpsync(self.renderFrameId)) { + const prevAndCurrInputs = self._generateInputFrameUpsync(noDelayInputFrameId); + prevSelfInput = prevAndCurrInputs[0]; + currSelfInput = prevAndCurrInputs[1]; + } + + const [prevRdf, rdf] = self.rollbackAndChase(self.renderFrameId, self.renderFrameId + 1, self.collisionSys, self.collisionSysMap, false); + self.applyRoomDownsyncFrameDynamics(rdf, prevRdf); + self.showDebugBoundaries(rdf); + ++self.renderFrameId; + self.lastRenderFrameIdTriggeredAt = performance.now(); + let t3 = performance.now(); + } catch (err) { + console.error("Error during Map.update", err); + } + } + }, +}); diff --git a/frontend/assets/scripts/OfflineMapBackend.js.meta b/frontend/assets/scripts/OfflineMapBackend.js.meta new file mode 100644 index 0000000..bb0072c --- /dev/null +++ b/frontend/assets/scripts/OfflineMapBackend.js.meta @@ -0,0 +1,9 @@ +{ + "ver": "1.0.5", + "uuid": "b3810903-496b-43d7-8461-898cee958548", + "isPlugin": false, + "loadPluginInWeb": true, + "loadPluginInNative": true, + "loadPluginInEditor": false, + "subMetas": {} +} \ No newline at end of file diff --git a/jsexport/main.go b/jsexport/main.go index 8908d3a..7bd1866 100644 --- a/jsexport/main.go +++ b/jsexport/main.go @@ -1,51 +1,100 @@ package main import ( - "github.com/gopherjs/gopherjs/js" + . "dnmshared" + . "dnmshared/sharedprotos" + "github.com/gopherjs/gopherjs/js" "github.com/solarlune/resolv" - . "jsexport/protos" - "jsexport/models" - . "dnmshared" + "jsexport/models" + . "jsexport/protos" ) -func NewRingBufferJs(n int32) *js.Object { - return js.MakeWrapper(NewRingBuffer(n)); -} - func NewCollisionSpaceJs(spaceW, spaceH, minStepW, minStepH int) *js.Object { - return js.MakeWrapper(resolv.NewSpace(spaceW, spaceH, minStepW, minStepH)) + return js.MakeWrapper(resolv.NewSpace(spaceW, spaceH, minStepW, minStepH)) } -func GenerateRectColliderJs(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, tag string) *js.Object { - /* - [WARNING] It's important to note that we don't need "js.MakeFullWrapper" for a call sequence as follows. - ``` - var space = gopkgs.NewCollisionSpaceJs(2048, 2048, 8, 8); - var a = gopkgs.GenerateRectColliderJs(189, 497, 48, 48, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, spaceOffsetX, spaceOffsetY, "Player"); - space.Add(a); - ``` - The "space" variable doesn't need access to the field of "a" in JavaScript level to run "space.Add(...)" method, which is good. - */ - return js.MakeWrapper(GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, tag)); +func NewVec2DJs(x, y float64) *js.Object { + return js.MakeFullWrapper(&Vec2D{ + X: x, + Y: y, + }) +} + +func NewPolygon2DJs(anchor *Vec2D, points []*Vec2D) *js.Object { + return js.MakeFullWrapper(&Polygon2D{ + Anchor: anchor, + Points: points, + }) +} + +func NewBarrierJs(boundary *Polygon2D) *js.Object { + return js.MakeWrapper(&Barrier{ + Boundary: boundary, + }) +} + +func NewPlayerDownsyncJs(id, virtualGridX, virtualGridY, dirX, dirY, velX, velY, speed, battleState, characterState, joinIndex, hp, maxHp int32, inAir bool, colliderRadius float64) *js.Object { + return js.MakeWrapper(&PlayerDownsync{ + Id: id, + VirtualGridX: virtualGridX, + VirtualGridY: virtualGridY, + DirX: dirX, + DirY: dirY, + VelX: velX, + VelY: velY, + Speed: speed, + BattleState: battleState, + JoinIndex: joinIndex, + ColliderRadius: colliderRadius, + Hp: hp, + MaxHp: maxHp, + CharacterState: characterState, + InAir: inAir, + }) +} + +func NewRoomDownsyncFrameJs(id int32, playersArr []*PlayerDownsync, meleeBullets []*MeleeBullet) *js.Object { + return js.MakeFullWrapper(&RoomDownsyncFrame{ + Id: id, + PlayersArr: playersArr, + MeleeBullets: meleeBullets, + }) +} + +func GenerateRectColliderJs(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY float64, data interface{}, tag string) *js.Object { + /* + [WARNING] It's important to note that we don't need "js.MakeFullWrapper" for a call sequence as follows. + ``` + var space = gopkgs.NewCollisionSpaceJs(2048, 2048, 8, 8); + var a = gopkgs.GenerateRectColliderJs(189, 497, 48, 48, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, spaceOffsetX, spaceOffsetY, "Player"); + space.Add(a); + ``` + The "space" variable doesn't need access to the field of "a" in JavaScript level to run "space.Add(...)" method, which is good. + */ + return js.MakeWrapper(GenerateRectCollider(wx, wy, w, h, topPadding, bottomPadding, leftPadding, rightPadding, spaceOffsetX, spaceOffsetY, data, tag)) } func CheckCollisionJs(obj *resolv.Object, dx, dy float64) *js.Object { - // TODO: Support multiple tags in the future - // Unfortunately I couldn't find a way to just call "var a = GenerateRectColliderJs(...); space.Add(a); a.Check(...)" to get the collision result, the unwrapped method will result in stack overflow. Need a better solution later. - return js.MakeFullWrapper(obj.Check(dx, dy)); + // TODO: Support multiple tags in the future + // Unfortunately I couldn't find a way to just call "var a = GenerateRectColliderJs(...); space.Add(a); a.Check(...)" to get the collision result, the unwrapped method will result in stack overflow. Need a better solution later. + return js.MakeFullWrapper(obj.Check(dx, dy)) } -func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, gravityX, gravityY, jumpingInitVelY, inputDelayFrames, inputScaleFrames int32, inputsBuffer *RingBuffer, collisionSpaceOffsetX, collisionSpaceOffsetY, snapIntoPlatformOverlap, snapIntoPlatformThreshold, worldToVirtualGridRatio, virtualGridToWorldRatio float64) *js.Object { - // We need access to all fields of RoomDownsyncFrame for displaying in frontend - return js.MakeFullWrapper(models.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap, gravityX, gravityY, jumpingInitVelY, inputDelayFrames, inputScaleFrames, inputsBuffer, collisionSpaceOffsetX, collisionSpaceOffsetY, snapIntoPlatformOverlap, snapIntoPlatformThreshold, worldToVirtualGridRatio, virtualGridToWorldRatio)) +func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(delayedInputFrame, delayedInputFrameForPrevRenderFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, gravityX, gravityY, jumpingInitVelY, inputDelayFrames, inputScaleFrames int32, collisionSpaceOffsetX, collisionSpaceOffsetY, snapIntoPlatformOverlap, snapIntoPlatformThreshold, worldToVirtualGridRatio, virtualGridToWorldRatio float64) *js.Object { + // We need access to all fields of RoomDownsyncFrame for displaying in frontend + return js.MakeFullWrapper(models.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, delayedInputFrameForPrevRenderFrame, currRenderFrame, collisionSys, collisionSysMap, gravityX, gravityY, jumpingInitVelY, inputDelayFrames, inputScaleFrames, collisionSpaceOffsetX, collisionSpaceOffsetY, snapIntoPlatformOverlap, snapIntoPlatformThreshold, worldToVirtualGridRatio, virtualGridToWorldRatio)) } func main() { js.Global.Set("gopkgs", map[string]interface{}{ - "NewRingBufferJs": NewRingBufferJs, - "NewCollisionSpaceJs": NewCollisionSpaceJs, - "GenerateRectColliderJs": GenerateRectColliderJs, - "CheckCollisionJs": CheckCollisionJs, + "NewVec2DJs": NewVec2DJs, + "NewPolygon2DJs": NewPolygon2DJs, + "NewBarrierJs": NewBarrierJs, + "NewPlayerDownsyncJs": NewPlayerDownsyncJs, + "NewRoomDownsyncFrameJs": NewRoomDownsyncFrameJs, + "NewCollisionSpaceJs": NewCollisionSpaceJs, + "GenerateRectColliderJs": GenerateRectColliderJs, + "CheckCollisionJs": CheckCollisionJs, }) } diff --git a/jsexport/models/battle.go b/jsexport/models/battle.go index 851bc72..b2fba67 100644 --- a/jsexport/models/battle.go +++ b/jsexport/models/battle.go @@ -1,10 +1,10 @@ package models import ( + . "dnmshared" + . "dnmshared/sharedprotos" "github.com/solarlune/resolv" - . "dnmshared/sharedprotos" - . "jsexport/protos" - . "dnmshared" + . "jsexport/protos" ) const ( @@ -81,10 +81,10 @@ func CalcHardPushbacksNorms(playerCollider *resolv.Object, playerShape *resolv.C return ret } -func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, gravityX, gravityY, jumpingInitVelY, inputDelayFrames, inputScaleFrames int32, inputsBuffer *RingBuffer, collisionSpaceOffsetX, collisionSpaceOffsetY, snapIntoPlatformOverlap, snapIntoPlatformThreshold, worldToVirtualGridRatio, virtualGridToWorldRatio float64) *RoomDownsyncFrame { +func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, delayedInputFrameForPrevRenderFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, gravityX, gravityY, jumpingInitVelY, inputDelayFrames, inputScaleFrames int32, collisionSpaceOffsetX, collisionSpaceOffsetY, snapIntoPlatformOverlap, snapIntoPlatformThreshold, worldToVirtualGridRatio, virtualGridToWorldRatio float64) *RoomDownsyncFrame { topPadding, bottomPadding, leftPadding, rightPadding := snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap // [WARNING] This function MUST BE called while "InputsBufferLock" is locked! - roomCapacity := len(currRenderFrame.PlayersArr) + roomCapacity := len(currRenderFrame.PlayersArr) nextRenderFramePlayers := make([]*PlayerDownsync, roomCapacity) // Make a copy first for i, currPlayerDownsync := range currRenderFrame.PlayersArr { @@ -117,11 +117,6 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *Input // 1. Process player inputs if nil != delayedInputFrame { - var delayedInputFrameForPrevRenderFrame *InputFrameDownsync = nil - tmp := inputsBuffer.GetByFrameId(ConvertToInputFrameId(currRenderFrame.Id-1, inputDelayFrames, inputScaleFrames)) - if nil != tmp { - delayedInputFrameForPrevRenderFrame = tmp.(*InputFrameDownsync) - } inputList := delayedInputFrame.InputList for i, currPlayerDownsync := range currRenderFrame.PlayersArr { joinIndex := currPlayerDownsync.JoinIndex @@ -150,16 +145,16 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *Input } } - // Note that by now "0 == thatPlayerInNextFrame.FramesToRecover", we should change "CharacterState" to "WALKING" or "IDLE" depending on player inputs - if 0 != decodedInput.Dx || 0 != decodedInput.Dy { - thatPlayerInNextFrame.DirX = decodedInput.Dx - thatPlayerInNextFrame.DirY = decodedInput.Dy - thatPlayerInNextFrame.VelX = decodedInput.Dx * currPlayerDownsync.Speed - thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING - } else { - thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1 - thatPlayerInNextFrame.VelX = 0 - } + // Note that by now "0 == thatPlayerInNextFrame.FramesToRecover", we should change "CharacterState" to "WALKING" or "IDLE" depending on player inputs + if 0 != decodedInput.Dx || 0 != decodedInput.Dy { + thatPlayerInNextFrame.DirX = decodedInput.Dx + thatPlayerInNextFrame.DirY = decodedInput.Dy + thatPlayerInNextFrame.VelX = decodedInput.Dx * currPlayerDownsync.Speed + thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING + } else { + thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1 + thatPlayerInNextFrame.VelX = 0 + } } } @@ -187,9 +182,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *Input } } - // 3. Invoke collision system stepping (no-op for backend collision lib) - - // 4. Calc pushbacks for each player (after its movement) w/o bullets + // 3. Calc pushbacks for each player (after its movement) w/o bullets for i, currPlayerDownsync := range currRenderFrame.PlayersArr { joinIndex := currPlayerDownsync.JoinIndex collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex @@ -201,6 +194,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *Input if collision := playerCollider.Check(0, 0); nil != collision { for _, obj := range collision.Objects { isBarrier, isAnotherPlayer, isBullet := false, false, false + // TODO: Make this part work in JavaScript without having to expose all types Barrier/PlayerDownsync/MeleeBullet by js.MakeWrapper. switch obj.Data.(type) { case *Barrier: isBarrier = true @@ -226,7 +220,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *Input thatPlayerInNextFrame.InAir = false } if isAnotherPlayer { - // [WARNING] The "zero overlap collision" might be randomly detected/missed on either frontend or backend, to have deterministic result we added paddings to all sides of a playerCollider. As each velocity component of (velX, velY) being a multiple of 0.5 at any renderFrame, each position component of (x, y) can only be a multiple of 0.5 too, thus whenever a 1-dimensional collision happens between players from [player#1: i*0.5, player#2: j*0.5, not collided yet] to [player#1: (i+k)*0.5, player#2: j*0.5, collided], the overlap becomes (i+k-j)*0.5+2*s, and after snapping subtraction the effPushback magnitude for each player is (i+k-j)*0.5, resulting in 0.5-multiples-position for the next renderFrame. + // [WARNING] The "zero overlap collision" might be randomly detected/missed on either frontend or backend, to have deterministic result we added paddings to all sides of a playerCollider. As each velocity component of (velX, velY) being a multiple of 0.5 at any renderFrame, each position component of (x, y) can only be a multiple of 0.5 too, thus whenever a 1-dimensional collision happens between players from [player#1: i*0.5, player#2: j*0.5, not collided yet] to [player#1: (i+k)*0.5, player#2: j*0.5, collided], the overlap becomes (i+k-j)*0.5+2*s, and after snapping subtraction the effPushback magnitude for each player is (i+k-j)*0.5, resulting in 0.5-multiples-position for the next renderFrame. pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap*2)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap*2)*overlapResult.OverlapY } for _, hardPushbackNorm := range hardPushbackNorms[joinIndex-1] { @@ -262,7 +256,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *Input } } - // 7. Get players out of stuck barriers if there's any + // 4. Get players out of stuck barriers if there's any for i, currPlayerDownsync := range currRenderFrame.PlayersArr { joinIndex := currPlayerDownsync.JoinIndex collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex @@ -274,7 +268,7 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *Input } return &RoomDownsyncFrame{ - Id: currRenderFrame.Id + 1, - PlayersArr: nextRenderFramePlayers, + Id: currRenderFrame.Id + 1, + PlayersArr: nextRenderFramePlayers, } }