mirror of
https://github.com/genxium/DelayNoMore
synced 2024-12-25 11:18:55 +00:00
Added necessary js type exposure to jsexport.
This commit is contained in:
parent
df5c9fda30
commit
8a9d449d83
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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)))
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -440,7 +440,7 @@
|
||||
"array": [
|
||||
0,
|
||||
0,
|
||||
209.73151519075364,
|
||||
215.95961841836203,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
|
1361
frontend/assets/scenes/offline_map_2.fire
Normal file
1361
frontend/assets/scenes/offline_map_2.fire
Normal file
File diff suppressed because it is too large
Load Diff
7
frontend/assets/scenes/offline_map_2.fire.meta
Normal file
7
frontend/assets/scenes/offline_map_2.fire.meta
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"ver": "1.2.5",
|
||||
"uuid": "8491a86c-bec9-4813-968a-128ca01639e0",
|
||||
"asyncLoadAssets": false,
|
||||
"autoReleaseAssets": false,
|
||||
"subMetas": {}
|
||||
}
|
238
frontend/assets/scripts/OfflineMapBackend.js
Normal file
238
frontend/assets/scripts/OfflineMapBackend.js
Normal file
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
9
frontend/assets/scripts/OfflineMapBackend.js.meta
Normal file
9
frontend/assets/scripts/OfflineMapBackend.js.meta
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"ver": "1.0.5",
|
||||
"uuid": "b3810903-496b-43d7-8461-898cee958548",
|
||||
"isPlugin": false,
|
||||
"loadPluginInWeb": true,
|
||||
"loadPluginInNative": true,
|
||||
"loadPluginInEditor": false,
|
||||
"subMetas": {}
|
||||
}
|
109
jsexport/main.go
109
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,
|
||||
})
|
||||
}
|
||||
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user