2022-09-20 15:50:01 +00:00
const i18n = require ( 'LanguageData' ) ;
i18n . init ( window . language ) ; // languageID should be equal to the one we input in New Language ID input field
2022-09-23 08:42:44 +00:00
const collisions = require ( './modules/Collisions' ) ;
2022-09-24 04:01:50 +00:00
const RingBuffer = require ( './RingBuffer' ) ;
2022-09-23 08:42:44 +00:00
2022-09-20 15:50:01 +00:00
window . ALL _MAP _STATES = {
VISUAL : 0 , // For free dragging & zooming.
EDITING _BELONGING : 1 ,
SHOWING _MODAL _POPUP : 2 ,
} ;
window . ALL _BATTLE _STATES = {
WAITING : 0 ,
IN _BATTLE : 1 ,
IN _SETTLEMENT : 2 ,
IN _DISMISSAL : 3 ,
} ;
window . MAGIC _ROOM _DOWNSYNC _FRAME _ID = {
2022-09-24 04:01:50 +00:00
BATTLE _READY _TO _START : - 1 ,
2022-10-01 15:54:48 +00:00
BATTLE _START : 0
2022-09-20 15:50:01 +00:00
} ;
2022-10-04 03:24:47 +00:00
window . PlayerBattleState = {
ADDED _PENDING _BATTLE _COLLIDER _ACK : 0 ,
READDED _PENDING _BATTLE _COLLIDER _ACK : 1 ,
ACTIVE : 2 ,
DISCONNECTED : 3 ,
LOST : 4 ,
EXPELLED _DURING _GAME : 5 ,
EXPELLED _IN _DISMISSAL : 6
} ;
2022-09-20 15:50:01 +00:00
cc . Class ( {
extends : cc . Component ,
properties : {
canvasNode : {
type : cc . Node ,
default : null ,
} ,
2022-11-19 12:58:07 +00:00
controlledCharacterPrefab : {
type : cc . Prefab ,
default : null ,
} ,
2022-09-20 15:50:01 +00:00
joystickInputControllerNode : {
type : cc . Node ,
default : null
} ,
confirmLogoutPrefab : {
type : cc . Prefab ,
default : null
} ,
simplePressToGoDialogPrefab : {
type : cc . Prefab ,
default : null
} ,
boundRoomIdLabel : {
type : cc . Label ,
default : null
} ,
countdownLabel : {
type : cc . Label ,
default : null
} ,
resultPanelPrefab : {
type : cc . Prefab ,
default : null
} ,
gameRulePrefab : {
type : cc . Prefab ,
default : null
} ,
findingPlayerPrefab : {
type : cc . Prefab ,
default : null
} ,
countdownToBeginGamePrefab : {
type : cc . Prefab ,
default : null
} ,
playersInfoPrefab : {
type : cc . Prefab ,
default : null
} ,
forceBigEndianFloatingNumDecoding : {
default : false ,
} ,
2022-10-04 03:24:47 +00:00
renderFrameIdLagTolerance : {
type : cc . Integer ,
default : 4 // implies (renderFrameIdLagTolerance >> inputScaleFrames) count of inputFrameIds
} ,
2022-11-10 10:18:00 +00:00
jigglingEps1D : {
2022-10-10 06:33:04 +00:00
type : cc . Float ,
default : 1e-3
} ,
2022-11-23 14:11:28 +00:00
bulletTriggerEnabled : {
default : false
} ,
2022-09-20 15:50:01 +00:00
} ,
2022-10-02 16:22:05 +00:00
2022-09-20 15:50:01 +00:00
_inputFrameIdDebuggable ( inputFrameId ) {
2022-10-02 16:22:05 +00:00
return ( 0 == inputFrameId % 10 ) ;
2022-09-20 15:50:01 +00:00
} ,
_convertToInputFrameId ( renderFrameId , inputDelayFrames ) {
if ( renderFrameId < inputDelayFrames ) return 0 ;
2022-09-21 15:59:05 +00:00
return ( ( renderFrameId - inputDelayFrames ) >> this . inputScaleFrames ) ;
2022-09-20 15:50:01 +00:00
} ,
2022-09-29 04:21:04 +00:00
_convertToFirstUsedRenderFrameId ( inputFrameId , inputDelayFrames ) {
2022-09-21 15:59:05 +00:00
return ( ( inputFrameId << this . inputScaleFrames ) + inputDelayFrames ) ;
2022-09-20 15:50:01 +00:00
} ,
2022-09-29 04:21:04 +00:00
shouldGenerateInputFrameUpsync ( renderFrameId ) {
2022-10-02 16:22:05 +00:00
return ( ( renderFrameId & ( ( 1 << this . inputScaleFrames ) - 1 ) ) == 0 ) ;
2022-09-20 15:50:01 +00:00
} ,
2022-09-24 04:01:50 +00:00
_allConfirmed ( confirmedList ) {
2022-10-02 16:22:05 +00:00
return ( confirmedList + 1 ) == ( 1 << this . playerRichInfoDict . size ) ;
} ,
2022-09-24 04:01:50 +00:00
2022-09-20 15:50:01 +00:00
_generateInputFrameUpsync ( inputFrameId ) {
2022-09-24 04:01:50 +00:00
const self = this ;
2022-09-20 15:50:01 +00:00
if (
2022-09-24 04:01:50 +00:00
null == self . ctrl ||
null == self . selfPlayerInfo
2022-09-20 15:50:01 +00:00
) {
2022-11-29 13:32:18 +00:00
throw ` noDelayInputFrameId= ${ inputFrameId } couldn't be generated: recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ;
2022-09-20 15:50:01 +00:00
}
2022-12-01 03:35:56 +00:00
let previousSelfInput = null ,
currSelfInput = null ;
2022-09-24 04:01:50 +00:00
const joinIndex = self . selfPlayerInfo . joinIndex ;
2022-12-01 03:35:56 +00:00
// [WARNING] The while-loop here handles a situation where the "resync rdf & accompaniedInputFrameDownsyncBatch" mismatched and we have to predict some "gap-inputFrames"!
while ( self . recentInputCache . edFrameId <= inputFrameId ) {
// TODO: find some kind of synchronization mechanism against "onInputFrameDownsyncBatch"!
const previousInputFrameDownsyncWithPrediction = self . getCachedInputFrameDownsyncWithPrediction ( inputFrameId - 1 ) ;
previousSelfInput = ( null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction . inputList [ joinIndex - 1 ] ) ;
// If "forceConfirmation" is active on backend, there's a chance that the already downsynced "inputFrameDownsync"s are ahead of a locally generating inputFrameId, in this case we respect the downsynced one.
const existingInputFrame = self . recentInputCache . getByFrameId ( inputFrameId ) ;
if ( null != existingInputFrame && self . _allConfirmed ( existingInputFrame . confirmedList ) ) {
console . log ( ` noDelayInputFrameId= ${ inputFrameId } already exists in recentInputCache and is all-confirmed: recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ) ;
return [ previousSelfInput , existingInputFrame . inputList [ joinIndex - 1 ] ] ;
}
const prefabbedInputList = ( null == previousInputFrameDownsyncWithPrediction ? new Array ( self . playerRichInfoDict . size ) . fill ( 0 ) : previousInputFrameDownsyncWithPrediction . inputList . slice ( ) ) ;
currSelfInput = self . ctrl . getEncodedInput ( ) ;
prefabbedInputList [ ( joinIndex - 1 ) ] = currSelfInput ;
const prefabbedInputFrameDownsync = window . pb . protos . InputFrameDownsync . create ( {
inputFrameId : self . recentInputCache . edFrameId ,
inputList : prefabbedInputList ,
confirmedList : ( 1 << ( self . selfPlayerInfo . joinIndex - 1 ) )
} ) ;
2022-09-20 15:50:01 +00:00
2022-12-01 03:35:56 +00:00
self . recentInputCache . put ( prefabbedInputFrameDownsync ) ; // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames"
2022-11-29 13:32:18 +00:00
}
2022-11-20 10:53:33 +00:00
return [ previousSelfInput , currSelfInput ] ;
2022-09-20 15:50:01 +00:00
} ,
2022-10-02 16:22:05 +00:00
2022-09-24 04:01:50 +00:00
shouldSendInputFrameUpsyncBatch ( prevSelfInput , currSelfInput , lastUpsyncInputFrameId , currInputFrameId ) {
2022-09-20 15:50:01 +00:00
/ *
2022-10-01 07:14:05 +00:00
For a 2 - player - battle , this "shouldUpsyncForEarlyAllConfirmedOnBackend" can be omitted , however for more players in a same battle , to avoid a "long time non-moving player" jamming the downsync of other moving players , we should use this flag .
When backend implements the "force confirmation" feature , we can have "false == shouldUpsyncForEarlyAllConfirmedOnBackend" all the time as well !
2022-09-20 15:50:01 +00:00
* /
if ( null == currSelfInput ) return false ;
2022-10-01 07:14:05 +00:00
2022-10-02 16:22:05 +00:00
const shouldUpsyncForEarlyAllConfirmedOnBackend = ( currInputFrameId - lastUpsyncInputFrameId >= this . inputFrameUpsyncDelayTolerance ) ;
2022-10-01 07:14:05 +00:00
return shouldUpsyncForEarlyAllConfirmedOnBackend || ( prevSelfInput != currSelfInput ) ;
2022-10-02 16:22:05 +00:00
} ,
2022-09-20 15:50:01 +00:00
2022-10-02 16:22:05 +00:00
sendInputFrameUpsyncBatch ( latestLocalInputFrameId ) {
// [WARNING] Why not just send the latest input? Because different player would have a different "latestLocalInputFrameId" of changing its last input, and that could make the server not recognizing any "all-confirmed inputFrame"!
2022-09-24 04:01:50 +00:00
const self = this ;
2022-09-20 15:50:01 +00:00
let inputFrameUpsyncBatch = [ ] ;
2022-10-02 16:22:05 +00:00
let batchInputFrameIdSt = self . lastUpsyncInputFrameId + 1 ;
if ( batchInputFrameIdSt < self . recentInputCache . stFrameId ) {
// Upon resync, "self.lastUpsyncInputFrameId" might not have been updated properly.
batchInputFrameIdSt = self . recentInputCache . stFrameId ;
}
for ( let i = batchInputFrameIdSt ; i <= latestLocalInputFrameId ; ++ i ) {
2022-09-24 04:01:50 +00:00
const inputFrameDownsync = self . recentInputCache . getByFrameId ( i ) ;
if ( null == inputFrameDownsync ) {
2022-11-29 13:32:18 +00:00
console . error ( ` sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=i: latestLocalInputFrameId= ${ latestLocalInputFrameId } , recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ) ;
2022-09-24 04:01:50 +00:00
} else {
const inputFrameUpsync = {
inputFrameId : i ,
2022-11-20 10:53:33 +00:00
encoded : inputFrameDownsync . inputList [ self . selfPlayerInfo . joinIndex - 1 ] ,
2022-09-24 04:01:50 +00:00
} ;
inputFrameUpsyncBatch . push ( inputFrameUpsync ) ;
}
2022-10-02 16:22:05 +00:00
}
2022-11-20 16:23:01 +00:00
2022-11-21 09:27:32 +00:00
// console.info(`inputFrameUpsyncBatch: ${JSON.stringify(inputFrameUpsyncBatch)}`);
2022-11-09 15:46:11 +00:00
const reqData = window . pb . protos . WsReq . encode ( {
2022-09-20 15:50:01 +00:00
msgId : Date . now ( ) ,
2022-09-24 04:01:50 +00:00
playerId : self . selfPlayerInfo . id ,
2022-09-20 15:50:01 +00:00
act : window . UPSYNC _MSG _ACT _PLAYER _CMD ,
2022-09-24 04:01:50 +00:00
joinIndex : self . selfPlayerInfo . joinIndex ,
ackingFrameId : self . lastAllConfirmedRenderFrameId ,
ackingInputFrameId : self . lastAllConfirmedInputFrameId ,
2022-09-20 15:50:01 +00:00
inputFrameUpsyncBatch : inputFrameUpsyncBatch ,
} ) . finish ( ) ;
window . sendSafely ( reqData ) ;
2022-10-02 16:22:05 +00:00
self . lastUpsyncInputFrameId = latestLocalInputFrameId ;
2022-11-29 13:32:18 +00:00
if ( self . lastUpsyncInputFrameId >= self . recentInputCache . edFrameId ) {
throw ` noDelayInputFrameId= ${ self . lastUpsyncInputFrameId } == latestLocalInputFrameId= ${ latestLocalInputFrameId } seems not properly dumped #2: recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ;
}
2022-09-20 15:50:01 +00:00
} ,
onEnable ( ) {
cc . log ( "+++++++ Map onEnable()" ) ;
} ,
onDisable ( ) {
cc . log ( "+++++++ Map onDisable()" ) ;
} ,
onDestroy ( ) {
const self = this ;
console . warn ( "+++++++ Map onDestroy()" ) ;
if ( null == self . battleState || ALL _BATTLE _STATES . WAITING == self . battleState ) {
window . clearBoundRoomIdInBothVolatileAndPersistentStorage ( ) ;
}
if ( null != window . handleBattleColliderInfo ) {
window . handleBattleColliderInfo = null ;
}
2022-10-25 15:02:39 +00:00
if ( null != window . handleClientSessionError ) {
window . handleClientSessionError = null ;
2022-09-20 15:50:01 +00:00
}
} ,
popupSimplePressToGo ( labelString , hideYesButton ) {
const self = this ;
self . state = ALL _MAP _STATES . SHOWING _MODAL _POPUP ;
const canvasNode = self . canvasNode ;
const simplePressToGoDialogNode = cc . instantiate ( self . simplePressToGoDialogPrefab ) ;
simplePressToGoDialogNode . setPosition ( cc . v2 ( 0 , 0 ) ) ;
simplePressToGoDialogNode . setScale ( 1 / canvasNode . scale ) ;
const simplePressToGoDialogScriptIns = simplePressToGoDialogNode . getComponent ( "SimplePressToGoDialog" ) ;
const yesButton = simplePressToGoDialogNode . getChildByName ( "Yes" ) ;
const postDismissalByYes = ( ) => {
self . transitToState ( ALL _MAP _STATES . VISUAL ) ;
canvasNode . removeChild ( simplePressToGoDialogNode ) ;
}
simplePressToGoDialogNode . getChildByName ( "Hint" ) . getComponent ( cc . Label ) . string = labelString ;
yesButton . once ( "click" , simplePressToGoDialogScriptIns . dismissDialog . bind ( simplePressToGoDialogScriptIns , postDismissalByYes ) ) ;
yesButton . getChildByName ( "Label" ) . getComponent ( cc . Label ) . string = "OK" ;
if ( true == hideYesButton ) {
yesButton . active = false ;
}
self . transitToState ( ALL _MAP _STATES . SHOWING _MODAL _POPUP ) ;
safelyAddChild ( self . widgetsAboveAllNode , simplePressToGoDialogNode ) ;
setLocalZOrder ( simplePressToGoDialogNode , 20 ) ;
return simplePressToGoDialogNode ;
} ,
alertForGoingBackToLoginScene ( labelString , mapIns , shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage ) {
const millisToGo = 3000 ;
mapIns . popupSimplePressToGo ( cc . js . formatStr ( "%s will logout in %s seconds." , labelString , millisToGo / 1000 ) ) ;
setTimeout ( ( ) => {
mapIns . logout ( false , shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage ) ;
} , millisToGo ) ;
} ,
_resetCurrentMatch ( ) {
const self = this ;
const mapNode = self . node ;
const canvasNode = mapNode . parent ;
2022-12-09 09:22:04 +00:00
2022-09-20 15:50:01 +00:00
// Clearing previous info of all players. [BEGINS]
2022-09-24 04:01:50 +00:00
self . collisionPlayerIndexPrefix = ( 1 << 17 ) ; // For tracking the movements of players
2022-09-22 04:45:17 +00:00
if ( null != self . playerRichInfoDict ) {
self . playerRichInfoDict . forEach ( ( playerRichInfo , playerId ) => {
if ( playerRichInfo . node . parent ) {
playerRichInfo . node . parent . removeChild ( playerRichInfo . node ) ;
2022-09-20 15:50:01 +00:00
}
2022-09-22 04:45:17 +00:00
} ) ;
2022-10-02 16:22:05 +00:00
}
2022-09-22 04:45:17 +00:00
self . playerRichInfoDict = new Map ( ) ;
2022-09-20 15:50:01 +00:00
// Clearing previous info of all players. [ENDS]
self . renderFrameId = 0 ; // After battle started
2022-11-22 09:12:51 +00:00
self . bulletBattleLocalIdCounter = 0 ;
2022-09-24 04:01:50 +00:00
self . lastAllConfirmedRenderFrameId = - 1 ;
self . lastAllConfirmedInputFrameId = - 1 ;
2022-09-20 15:50:01 +00:00
self . lastUpsyncInputFrameId = - 1 ;
2022-10-02 16:22:05 +00:00
self . chaserRenderFrameId = - 1 ; // at any moment, "lastAllConfirmedRenderFrameId <= chaserRenderFrameId <= renderFrameId", but "chaserRenderFrameId" would fluctuate according to "onInputFrameDownsyncBatch"
2022-09-20 15:50:01 +00:00
2022-11-23 04:30:30 +00:00
self . recentRenderCache = new RingBuffer ( self . renderCacheSize ) ;
2022-09-20 15:50:01 +00:00
self . selfPlayerInfo = null ; // This field is kept for distinguishing "self" and "others".
2022-11-30 16:30:35 +00:00
self . recentInputCache = new RingBuffer ( ( self . renderCacheSize >> 1 ) + 1 ) ;
2022-09-23 08:42:44 +00:00
2022-11-11 05:27:48 +00:00
self . collisionSys = new collisions . Collisions ( ) ;
2022-09-24 04:01:50 +00:00
self . collisionBarrierIndexPrefix = ( 1 << 16 ) ; // For tracking the movements of barriers, though not yet actually used
2022-11-22 09:12:51 +00:00
self . collisionBulletIndexPrefix = ( 1 << 15 ) ; // For tracking the movements of bullets
2022-11-11 05:27:48 +00:00
self . collisionSysMap = new Map ( ) ;
2022-10-02 16:22:05 +00:00
2022-12-01 03:35:56 +00:00
console . log ( ` collisionSys & collisionSysMap reset ` ) ;
2022-09-20 15:50:01 +00:00
self . transitToState ( ALL _MAP _STATES . VISUAL ) ;
self . battleState = ALL _BATTLE _STATES . WAITING ;
2022-11-23 14:11:28 +00:00
self . countdownNanos = null ;
2022-11-19 12:58:07 +00:00
if ( self . countdownLabel ) {
self . countdownLabel . string = "" ;
}
2022-09-20 15:50:01 +00:00
if ( self . findingPlayerNode ) {
const findingPlayerScriptIns = self . findingPlayerNode . getComponent ( "FindingPlayer" ) ;
findingPlayerScriptIns . init ( ) ;
}
2022-11-19 12:58:07 +00:00
if ( self . playersInfoNode ) {
safelyAddChild ( self . widgetsAboveAllNode , self . playersInfoNode ) ;
}
if ( self . findingPlayerNode ) {
safelyAddChild ( self . widgetsAboveAllNode , self . findingPlayerNode ) ;
}
2022-09-20 15:50:01 +00:00
} ,
onLoad ( ) {
const self = this ;
window . mapIns = self ;
window . forceBigEndianFloatingNumDecoding = self . forceBigEndianFloatingNumDecoding ;
2022-12-15 04:28:39 +00:00
self . showCriticalCoordinateLabels = false ;
2022-11-12 12:34:38 +00:00
2022-09-20 15:50:01 +00:00
console . warn ( "+++++++ Map onLoad()" ) ;
2022-10-25 15:02:39 +00:00
window . handleClientSessionError = function ( ) {
console . warn ( '+++++++ Common handleClientSessionError()' ) ;
2022-09-20 15:50:01 +00:00
2022-10-04 03:24:47 +00:00
if ( ALL _BATTLE _STATES . IN _SETTLEMENT == self . battleState ) {
console . log ( "Battled ended by settlement" ) ;
2022-09-20 15:50:01 +00:00
} else {
2022-10-04 03:24:47 +00:00
console . warn ( "Connection lost, going back to login page" ) ;
2022-09-20 15:50:01 +00:00
window . clearLocalStorageAndBackToLoginScene ( true ) ;
}
} ;
const mapNode = self . node ;
const canvasNode = mapNode . parent ;
2022-09-23 08:42:44 +00:00
cc . director . getCollisionManager ( ) . enabled = false ;
2022-09-20 15:50:01 +00:00
// self.musicEffectManagerScriptIns = self.node.getComponent("MusicEffectManager");
self . musicEffectManagerScriptIns = null ;
/** Init required prefab started. */
self . confirmLogoutNode = cc . instantiate ( self . confirmLogoutPrefab ) ;
self . confirmLogoutNode . getComponent ( "ConfirmLogout" ) . mapNode = self . node ;
// Initializes Result panel.
self . resultPanelNode = cc . instantiate ( self . resultPanelPrefab ) ;
self . resultPanelNode . width = self . canvasNode . width ;
self . resultPanelNode . height = self . canvasNode . height ;
const resultPanelScriptIns = self . resultPanelNode . getComponent ( "ResultPanel" ) ;
resultPanelScriptIns . mapScriptIns = self ;
resultPanelScriptIns . onAgainClicked = ( ) => {
2022-10-02 16:22:05 +00:00
self . battleState = ALL _BATTLE _STATES . WAITING ;
2022-09-20 15:50:01 +00:00
window . clearBoundRoomIdInBothVolatileAndPersistentStorage ( ) ;
window . initPersistentSessionClient ( self . initAfterWSConnected , null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ ) ;
} ;
2022-12-01 03:35:56 +00:00
resultPanelScriptIns . onCloseDelegate = ( ) => { } ;
2022-09-20 15:50:01 +00:00
self . gameRuleNode = cc . instantiate ( self . gameRulePrefab ) ;
self . gameRuleNode . width = self . canvasNode . width ;
self . gameRuleNode . height = self . canvasNode . height ;
self . gameRuleScriptIns = self . gameRuleNode . getComponent ( "GameRule" ) ;
self . gameRuleScriptIns . mapNode = self . node ;
self . findingPlayerNode = cc . instantiate ( self . findingPlayerPrefab ) ;
self . findingPlayerNode . width = self . canvasNode . width ;
self . findingPlayerNode . height = self . canvasNode . height ;
const findingPlayerScriptIns = self . findingPlayerNode . getComponent ( "FindingPlayer" ) ;
findingPlayerScriptIns . init ( ) ;
self . playersInfoNode = cc . instantiate ( self . playersInfoPrefab ) ;
self . countdownToBeginGameNode = cc . instantiate ( self . countdownToBeginGamePrefab ) ;
self . countdownToBeginGameNode . width = self . canvasNode . width ;
self . countdownToBeginGameNode . height = self . canvasNode . height ;
self . mainCameraNode = 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. */
window . handleBattleColliderInfo = function ( parsedBattleColliderInfo ) {
2022-12-01 03:35:56 +00:00
console . log ( ` Received parsedBattleColliderInfo via ws ` ) ;
2022-11-29 13:32:18 +00:00
// TODO: Upon reconnection, the backend might have already been sending down data that'd trigger "onRoomDownsyncFrame & onInputFrameDownsyncBatch", but frontend could reject those data due to "battleState != PlayerBattleState.ACTIVE".
2022-11-22 09:12:51 +00:00
Object . assign ( self , parsedBattleColliderInfo ) ;
2022-12-12 11:11:59 +00:00
self . gravityX = parsedBattleColliderInfo . gravityX ; // to avoid integer default value 0 accidentally becoming null in "Object.assign(...)"
2022-11-29 13:32:18 +00:00
self . tooFastDtIntervalMillis = 0.5 * self . rollbackEstimatedDtMillis ;
2022-11-09 10:13:53 +00:00
2022-09-20 15:50:01 +00:00
const tiledMapIns = self . node . getComponent ( cc . TiledMap ) ;
2022-11-16 14:11:56 +00:00
// It's easier to just use the "barrier"s extracted by the backend (all anchor points in world coordinates), but I'd like to verify frontend tmx parser logic as well.
2022-09-20 15:50:01 +00:00
const fullPathOfTmxFile = cc . js . formatStr ( "map/%s/map" , parsedBattleColliderInfo . stageName ) ;
cc . loader . loadRes ( fullPathOfTmxFile , cc . TiledMapAsset , ( err , tmxAsset ) => {
if ( null != err ) {
console . error ( err ) ;
return ;
}
2022-10-02 16:22:05 +00:00
2022-09-20 15:50:01 +00:00
/ *
[ WARNING ]
- The order of the following statements is important , because we should have finished "_resetCurrentMatch" before the first "RoomDownsyncFrame" .
2022-09-24 04:01:50 +00:00
- It ' s important to assign new "tmxAsset" before "extractBoundaryObjects" , to ensure that the correct tilesets are used .
2022-09-20 15:50:01 +00:00
- To ensure clearance , put destruction of the "cc.TiledMap" component preceding that of "mapNode.destroyAllChildren()" .
* /
tiledMapIns . tmxAsset = null ;
mapNode . removeAllChildren ( ) ;
2022-10-02 16:22:05 +00:00
self . _resetCurrentMatch ( ) ;
2022-09-20 15:50:01 +00:00
2022-12-14 13:30:01 +00:00
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 ;
}
2022-09-20 15:50:01 +00:00
tiledMapIns . tmxAsset = tmxAsset ;
const newMapSize = tiledMapIns . getMapSize ( ) ;
const newTileSize = tiledMapIns . getTileSize ( ) ;
2022-10-02 16:22:05 +00:00
self . node . setContentSize ( newMapSize . width * newTileSize . width , newMapSize . height * newTileSize . height ) ;
2022-09-20 15:50:01 +00:00
self . node . setPosition ( cc . v2 ( 0 , 0 ) ) ;
/ *
* Deliberately hiding "ImageLayer" s . This dirty fix is specific to "CocosCreator v2.2.1" , where it got back the rendering capability of "ImageLayer of Tiled" , yet made incorrectly . In this game our "markers of ImageLayers" are rendered by dedicated prefabs with associated colliders .
*
* -- YFLu , 2020 - 01 - 23
* /
const existingImageLayers = tiledMapIns . getObjectGroups ( ) ;
for ( let singleImageLayer of existingImageLayers ) {
2022-10-02 16:22:05 +00:00
singleImageLayer . node . opacity = 0 ;
2022-09-20 15:50:01 +00:00
}
2022-09-24 04:01:50 +00:00
let barrierIdCounter = 0 ;
2022-12-14 13:30:01 +00:00
const refBoundaryObjs = tileCollisionManager . extractBoundaryObjects ( self . node ) . barriers ;
const boundaryObjs = parsedBattleColliderInfo . strToPolygon2DListMap ;
for ( let k = 0 ; k < boundaryObjs [ "Barrier" ] . eles . length ; k ++ ) {
2022-12-15 04:28:39 +00:00
let boundaryObj = boundaryObjs [ "Barrier" ] . eles [ k ] ;
const refBoundaryObj = refBoundaryObjs [ k ] ;
2022-12-14 13:30:01 +00:00
// boundaryObj = refBoundaryObj;
const [ x0 , y0 ] = [ boundaryObj . anchor . x , boundaryObj . anchor . y ] ;
const newBarrierCollider = self . collisionSys . createPolygon ( x0 , y0 , Array . from ( boundaryObj . points , p => {
2022-11-19 12:58:07 +00:00
return [ p . x , p . y ] ;
} ) ) ;
2022-12-12 15:17:54 +00:00
newBarrierCollider . data = {
hardPushback : true
} ;
2022-11-16 14:11:56 +00:00
2022-11-13 03:37:30 +00:00
if ( self . showCriticalCoordinateLabels ) {
2022-11-16 14:11:56 +00:00
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 ;
2022-11-19 12:58:07 +00:00
barrierVertLabel . lineHeight = barrierVertLabel . fontSize + 1 ;
2022-11-16 14:11:56 +00:00
barrierVertLabel . string = ` ( ${ wx . toFixed ( 1 ) } , ${ wy . toFixed ( 1 ) } ) ` ;
safelyAddChild ( self . node , barrierVertLabelNode ) ;
setLocalZOrder ( barrierVertLabelNode , 5 ) ;
barrierVertLabelNode . active = true ;
2022-11-12 12:34:38 +00:00
}
2022-11-16 14:11:56 +00:00
2022-10-02 16:22:05 +00:00
}
2022-09-24 04:01:50 +00:00
++ barrierIdCounter ;
2022-10-02 16:22:05 +00:00
const collisionBarrierIndex = ( self . collisionBarrierIndexPrefix + barrierIdCounter ) ;
2022-12-01 03:35:56 +00:00
self . collisionSysMap . set ( collisionBarrierIndex , newBarrierCollider ) ;
2022-12-01 04:17:30 +00:00
// console.log(`Created new barrier collider: ${collisionBarrierIndex}`);
2022-09-24 04:01:50 +00:00
}
2022-09-20 15:50:01 +00:00
self . selfPlayerInfo = JSON . parse ( cc . sys . localStorage . getItem ( 'selfPlayer' ) ) ;
Object . assign ( self . selfPlayerInfo , {
id : self . selfPlayerInfo . playerId
} ) ;
2022-11-16 14:11:56 +00:00
const reqData = window . pb . protos . WsReq . encode ( {
msgId : Date . now ( ) ,
act : window . UPSYNC _MSG _ACT _PLAYER _COLLIDER _ACK ,
} ) . finish ( ) ;
window . sendSafely ( reqData ) ;
2022-12-01 03:35:56 +00:00
console . log ( ` Sent UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK via ws ` ) ;
2022-09-20 15:50:01 +00:00
} ) ;
} ;
self . initAfterWSConnected = ( ) => {
const self = window . mapIns ;
self . hideGameRuleNode ( ) ;
self . transitToState ( ALL _MAP _STATES . WAITING ) ;
self . _inputControlEnabled = false ;
}
// The player is now viewing "self.gameRuleNode" with button(s) to start an actual battle. -- YFLu
const expectedRoomId = window . getExpectedRoomIdSync ( ) ;
const boundRoomId = window . getBoundRoomIdFromPersistentStorage ( ) ;
console . warn ( "Map.onLoad, expectedRoomId == " , expectedRoomId , ", boundRoomId == " , boundRoomId ) ;
if ( null != expectedRoomId ) {
self . disableGameRuleNode ( ) ;
// The player is now possibly viewing "self.gameRuleNode" with no button, and should wait for `self.initAfterWSConnected` to be called.
2022-10-02 16:22:05 +00:00
self . battleState = ALL _BATTLE _STATES . WAITING ;
2022-09-20 15:50:01 +00:00
window . initPersistentSessionClient ( self . initAfterWSConnected , expectedRoomId ) ;
} else if ( null != boundRoomId ) {
self . disableGameRuleNode ( ) ;
2022-10-02 16:22:05 +00:00
self . battleState = ALL _BATTLE _STATES . WAITING ;
2022-10-04 03:24:47 +00:00
window . initPersistentSessionClient ( self . initAfterWSConnected , boundRoomId ) ;
2022-09-20 15:50:01 +00:00
} else {
self . showPopupInCanvas ( self . gameRuleNode ) ;
2022-11-25 05:24:03 +00:00
// Deliberately left blank. -- YFLu
2022-09-20 15:50:01 +00:00
}
} ,
disableGameRuleNode ( ) {
const self = window . mapIns ;
if ( null == self . gameRuleNode ) {
return ;
}
if ( null == self . gameRuleScriptIns ) {
return ;
}
if ( null == self . gameRuleScriptIns . modeButton ) {
return ;
}
self . gameRuleScriptIns . modeButton . active = false ;
} ,
hideGameRuleNode ( ) {
const self = window . mapIns ;
if ( null == self . gameRuleNode ) {
return ;
}
self . gameRuleNode . active = false ;
} ,
enableInputControls ( ) {
this . _inputControlEnabled = true ;
} ,
disableInputControls ( ) {
this . _inputControlEnabled = false ;
} ,
2022-11-30 16:30:35 +00:00
onRoomDownsyncFrame ( rdf , accompaniedInputFrameDownsyncBatch ) {
2022-09-21 04:21:36 +00:00
// This function is also applicable to "re-joining".
2022-09-20 15:50:01 +00:00
const self = window . mapIns ;
2022-11-29 13:32:18 +00:00
if ( ! self . recentRenderCache ) {
return ;
}
if ( ALL _BATTLE _STATES . IN _SETTLEMENT == self . battleState ) {
return ;
2022-10-03 03:42:19 +00:00
}
2022-11-29 13:32:18 +00:00
const shouldForceDumping1 = ( window . MAGIC _ROOM _DOWNSYNC _FRAME _ID . BATTLE _START == rdf . id ) ;
const shouldForceDumping2 = ( rdf . id > self . renderFrameId + self . renderFrameIdLagTolerance ) ;
2022-11-08 13:38:23 +00:00
2022-12-01 03:35:56 +00:00
const [ dumpRenderCacheRet , oldStRenderFrameId , oldEdRenderFrameId ] = ( shouldForceDumping1 || shouldForceDumping2 ) ? self . recentRenderCache . setByFrameId ( rdf , rdf . id ) : [ window . RING _BUFF _CONSECUTIVE _SET , null , null ] ;
2022-10-02 16:22:05 +00:00
if ( window . RING _BUFF _FAILED _TO _SET == dumpRenderCacheRet ) {
2022-11-29 13:32:18 +00:00
throw ` Failed to dump render cache#1 (maybe recentRenderCache too small)! rdf.id= ${ rdf . id } , lastAllConfirmedRenderFrameId= ${ self . lastAllConfirmedRenderFrameId } , lastAllConfirmedInputFrameId= ${ self . lastAllConfirmedInputFrameId } ; recentRenderCache= ${ self . _stringifyRecentRenderCache ( false ) } , recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ;
2022-10-02 16:22:05 +00:00
}
if ( window . MAGIC _ROOM _DOWNSYNC _FRAME _ID . BATTLE _START < rdf . id && window . RING _BUFF _CONSECUTIVE _SET == dumpRenderCacheRet ) {
2022-10-03 03:42:19 +00:00
/ *
Don ' t change
2022-11-10 16:01:53 +00:00
- lastAllConfirmedRenderFrameId , it ' s updated only in "rollbackAndChase" ( except for when RING _BUFF _NON _CONSECUTIVE _SET )
2022-11-11 05:27:48 +00:00
- chaserRenderFrameId , it ' s updated only in "rollbackAndChase & onInputFrameDownsyncBatch" ( except for when RING _BUFF _NON _CONSECUTIVE _SET )
2022-10-03 03:42:19 +00:00
* /
2022-10-02 16:22:05 +00:00
return dumpRenderCacheRet ;
}
2022-11-29 13:32:18 +00:00
// The logic below applies to (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet)
2022-09-26 02:36:46 +00:00
const players = rdf . players ;
2022-11-21 09:27:32 +00:00
self . _initPlayerRichInfoDict ( players ) ;
2022-09-26 02:36:46 +00:00
// Show the top status indicators for IN_BATTLE
2022-11-19 12:58:07 +00:00
if ( self . playersInfoNode ) {
const playersInfoScriptIns = self . playersInfoNode . getComponent ( "PlayersInfo" ) ;
2022-11-21 09:27:32 +00:00
for ( let i in players ) {
playersInfoScriptIns . updateData ( players [ i ] ) ;
2022-11-19 12:58:07 +00:00
}
2022-09-26 02:36:46 +00:00
}
2022-11-25 05:24:03 +00:00
if ( null == self . renderFrameId || self . renderFrameId <= rdf . id ) {
// In fact, not having "window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet" should already imply that "self.renderFrameId <= rdf.id", but here we double check and log the anomaly
2022-11-29 13:32:18 +00:00
if ( window . MAGIC _ROOM _DOWNSYNC _FRAME _ID . BATTLE _START == rdf . id ) {
console . log ( 'On battle started! renderFrameId=' , rdf . id ) ;
} else {
2022-12-01 03:35:56 +00:00
self . hideFindingPlayersGUI ( rdf ) ;
2022-11-30 16:30:35 +00:00
self . onInputFrameDownsyncBatch ( accompaniedInputFrameDownsyncBatch ) ; // Important to do this step before setting IN_BATTLE
2022-11-29 13:32:18 +00:00
console . warn ( ` Got resync@localRenderFrameId= ${ self . renderFrameId } -> rdf.id= ${ rdf . id } & rdf.backendUnconfirmedMask= ${ rdf . backendUnconfirmedMask } , @lastAllConfirmedRenderFrameId= ${ self . lastAllConfirmedRenderFrameId } , @lastAllConfirmedInputFrameId= ${ self . lastAllConfirmedInputFrameId } , @chaserRenderFrameId= ${ self . chaserRenderFrameId } , @localRecentInputCache= ${ mapIns . _stringifyRecentInputCache ( false ) } ` ) ;
}
2022-11-25 05:24:03 +00:00
self . renderFrameId = rdf . id ;
self . lastRenderFrameIdTriggeredAt = performance . now ( ) ;
// In this case it must be true that "rdf.id > chaserRenderFrameId >= lastAllConfirmedRenderFrameId".
self . lastAllConfirmedRenderFrameId = rdf . id ;
self . chaserRenderFrameId = rdf . id ;
2022-11-29 13:32:18 +00:00
const candidateLastAllConfirmedInputFrame = self . _convertToInputFrameId ( rdf . id - 1 , self . inputDelayFrames ) ;
if ( self . lastAllConfirmedInputFrame < candidateLastAllConfirmedInputFrame ) {
self . lastAllConfirmedInputFrame = candidateLastAllConfirmedInputFrame ;
}
2022-11-25 05:24:03 +00:00
const canvasNode = self . canvasNode ;
self . ctrl = canvasNode . getComponent ( "TouchEventsManager" ) ;
self . enableInputControls ( ) ;
self . transitToState ( ALL _MAP _STATES . VISUAL ) ;
self . battleState = ALL _BATTLE _STATES . IN _BATTLE ;
2022-11-09 15:46:11 +00:00
2022-11-25 05:24:03 +00:00
if ( self . countdownToBeginGameNode && self . countdownToBeginGameNode . parent ) {
self . countdownToBeginGameNode . parent . removeChild ( self . countdownToBeginGameNode ) ;
}
if ( null != self . musicEffectManagerScriptIns ) {
self . musicEffectManagerScriptIns . playBGM ( ) ;
}
} else {
2022-11-25 09:57:10 +00:00
console . warn ( ` Anomaly when onRoomDownsyncFrame is called by rdf= ${ JSON . stringify ( rdf ) } , recentRenderCache= ${ self . _stringifyRecentRenderCache ( false ) } , recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ) ;
2022-09-20 15:50:01 +00:00
}
2022-10-02 16:22:05 +00:00
2022-11-25 05:24:03 +00:00
// [WARNING] Leave all graphical updates in "update(dt)" by "applyRoomDownsyncFrameDynamics"
2022-10-02 16:22:05 +00:00
return dumpRenderCacheRet ;
} ,
equalInputLists ( lhs , rhs ) {
if ( null == lhs || null == rhs ) return false ;
if ( lhs . length != rhs . length ) return false ;
for ( let i in lhs ) {
if ( lhs [ i ] == rhs [ i ] ) continue ;
return false ;
}
return true ;
} ,
2022-10-16 02:38:38 +00:00
onInputFrameDownsyncBatch ( batch ) {
2022-12-01 03:35:56 +00:00
// TODO: find some kind of synchronization mechanism against "_generateInputFrameUpsync"!
2022-10-02 16:22:05 +00:00
const self = this ;
2022-11-29 13:32:18 +00:00
if ( ! self . recentInputCache ) {
return ;
}
if ( ALL _BATTLE _STATES . IN _SETTLEMENT == self . battleState ) {
2022-10-02 16:22:05 +00:00
return ;
}
let firstPredictedYetIncorrectInputFrameId = null ;
for ( let k in batch ) {
const inputFrameDownsync = batch [ k ] ;
const inputFrameDownsyncId = inputFrameDownsync . inputFrameId ;
2022-10-03 03:42:19 +00:00
if ( inputFrameDownsyncId < self . lastAllConfirmedInputFrameId ) {
continue ;
}
2022-11-29 13:32:18 +00:00
self . lastAllConfirmedInputFrameId = inputFrameDownsyncId ;
2022-10-16 02:38:38 +00:00
const localInputFrame = self . recentInputCache . getByFrameId ( inputFrameDownsyncId ) ;
if ( null != localInputFrame
&&
null == firstPredictedYetIncorrectInputFrameId
&&
! self . equalInputLists ( localInputFrame . inputList , inputFrameDownsync . inputList )
) {
firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId ;
2022-10-02 16:22:05 +00:00
}
2022-11-11 12:10:43 +00:00
// [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase".
inputFrameDownsync . confirmedList = ( 1 << self . playerRichInfoDict . size ) - 1 ;
2022-12-01 03:35:56 +00:00
const [ ret , oldStFrameId , oldEdFrameId ] = self . recentInputCache . setByFrameId ( inputFrameDownsync , inputFrameDownsync . inputFrameId ) ;
if ( window . RING _BUFF _FAILED _TO _SET == ret ) {
throw ` Failed to dump input cache (maybe recentInputCache too small)! inputFrameDownsync.inputFrameId= ${ inputFrameDownsync . inputFrameId } , lastAllConfirmedRenderFrameId= ${ self . lastAllConfirmedRenderFrameId } , lastAllConfirmedInputFrameId= ${ self . lastAllConfirmedInputFrameId } ; recentRenderCache= ${ self . _stringifyRecentRenderCache ( false ) } , recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ;
}
2022-10-02 16:22:05 +00:00
}
2022-10-16 02:38:38 +00:00
if ( null == firstPredictedYetIncorrectInputFrameId ) return ;
const inputFrameId1 = firstPredictedYetIncorrectInputFrameId ;
const renderFrameId1 = self . _convertToFirstUsedRenderFrameId ( inputFrameId1 , self . inputDelayFrames ) ; // a.k.a. "firstRenderFrameIdUsingIncorrectInputFrameId"
if ( renderFrameId1 >= self . renderFrameId ) return ; // No need to rollback when "renderFrameId1 == self.renderFrameId", because the "corresponding delayedInputFrame for renderFrameId1" is NOT YET EXECUTED BY NOW, it just went through "++self.renderFrameId" in "update(dt)" and javascript-runtime is mostly single-threaded in our programmable range.
2022-10-02 16:22:05 +00:00
2022-10-16 02:38:38 +00:00
if ( renderFrameId1 >= self . chaserRenderFrameId ) return ;
2022-10-02 16:22:05 +00:00
2022-10-16 02:38:38 +00:00
/ *
A typical case is as follows .
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
[ self . lastAllConfirmedRenderFrameId ] : 22
2022-10-02 16:22:05 +00:00
2022-10-16 02:38:38 +00:00
< renderFrameId1 > : 36
2022-10-02 16:22:05 +00:00
2022-10-16 02:38:38 +00:00
< self . chaserRenderFrameId > : 62
[ self . renderFrameId ] : 64
-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- --
* /
// The actual rollback-and-chase would later be executed in update(dt).
2022-11-11 15:43:51 +00:00
console . warn ( ` Mismatched input detected, resetting chaserRenderFrameId: ${ self . chaserRenderFrameId } -> ${ renderFrameId1 } by firstPredictedYetIncorrectInputFrameId: ${ inputFrameId1 } ` ) ;
2022-10-16 02:38:38 +00:00
self . chaserRenderFrameId = renderFrameId1 ;
2022-09-20 15:50:01 +00:00
} ,
2022-10-02 16:22:05 +00:00
onPlayerAdded ( rdf ) {
const self = this ;
// Update the "finding player" GUI and show it if not previously present
if ( ! self . findingPlayerNode . parent ) {
self . showPopupInCanvas ( self . findingPlayerNode ) ;
}
let findingPlayerScriptIns = self . findingPlayerNode . getComponent ( "FindingPlayer" ) ;
2022-11-21 09:27:32 +00:00
findingPlayerScriptIns . updatePlayersInfo ( rdf . players ) ;
2022-10-02 16:22:05 +00:00
} ,
2022-09-20 15:50:01 +00:00
logBattleStats ( ) {
const self = this ;
let s = [ ] ;
2022-11-29 13:32:18 +00:00
s . push ( ` Battle stats: renderFrameId= ${ self . renderFrameId } , lastAllConfirmedRenderFrameId= ${ self . lastAllConfirmedRenderFrameId } , lastUpsyncInputFrameId= ${ self . lastUpsyncInputFrameId } , lastAllConfirmedInputFrameId= ${ self . lastAllConfirmedInputFrameId } , chaserRenderFrameId= ${ self . chaserRenderFrameId } ; recentRenderCache= ${ self . _stringifyRecentRenderCache ( false ) } , recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ) ;
2022-10-02 16:22:05 +00:00
2022-09-25 12:48:09 +00:00
for ( let i = self . recentInputCache . stFrameId ; i < self . recentInputCache . edFrameId ; ++ i ) {
2022-10-02 16:22:05 +00:00
const inputFrameDownsync = self . recentInputCache . getByFrameId ( i ) ;
2022-09-22 09:09:49 +00:00
s . push ( JSON . stringify ( inputFrameDownsync ) ) ;
2022-10-02 16:22:05 +00:00
}
2022-09-20 15:50:01 +00:00
console . log ( s . join ( '\n' ) ) ;
} ,
onBattleStopped ( ) {
const self = this ;
2022-10-02 16:22:05 +00:00
if ( ALL _BATTLE _STATES . IN _BATTLE != self . battleState ) {
return ;
}
2022-09-20 15:50:01 +00:00
self . countdownNanos = null ;
self . logBattleStats ( ) ;
if ( self . musicEffectManagerScriptIns ) {
self . musicEffectManagerScriptIns . stopAllMusic ( ) ;
}
const canvasNode = self . canvasNode ;
const resultPanelNode = self . resultPanelNode ;
const resultPanelScriptIns = resultPanelNode . getComponent ( "ResultPanel" ) ;
resultPanelScriptIns . showPlayerInfo ( self . playerRichInfoDict ) ;
window . clearBoundRoomIdInBothVolatileAndPersistentStorage ( ) ;
self . battleState = ALL _BATTLE _STATES . IN _SETTLEMENT ;
self . showPopupInCanvas ( resultPanelNode ) ;
// Clear player info
self . playersInfoNode . getComponent ( "PlayersInfo" ) . clearInfo ( ) ;
} ,
2022-11-23 14:11:28 +00:00
spawnPlayerNode ( joinIndex , vx , vy , playerDownsyncInfo ) {
2022-09-24 04:01:50 +00:00
const self = this ;
2022-11-19 14:59:12 +00:00
const newPlayerNode = cc . instantiate ( self . controlledCharacterPrefab )
2022-11-19 12:58:07 +00:00
const playerScriptIns = newPlayerNode . getComponent ( "ControlledCharacter" ) ;
2022-11-19 14:59:12 +00:00
if ( 1 == joinIndex ) {
2022-11-22 09:12:51 +00:00
playerScriptIns . setSpecies ( "SoldierWaterGhost" ) ;
2022-11-20 10:53:33 +00:00
} else if ( 2 == joinIndex ) {
2022-12-15 04:28:39 +00:00
playerScriptIns . setSpecies ( "SoldierFireGhostFrameAnim" ) ;
2022-11-19 14:59:12 +00:00
}
2022-11-25 03:20:05 +00:00
const [ wx , wy ] = self . virtualGridToWorldPos ( vx , vy ) ;
newPlayerNode . setPosition ( wx , wy ) ;
2022-11-19 12:58:07 +00:00
playerScriptIns . mapNode = self . node ;
2022-12-16 05:55:43 +00:00
const halfColliderWidth = playerDownsyncInfo . colliderRadius ,
halfColliderHeight = playerDownsyncInfo . colliderRadius + playerDownsyncInfo . colliderRadius ; // avoid multiplying
const colliderWidth = halfColliderWidth + halfColliderWidth ,
colliderHeight = halfColliderHeight + halfColliderHeight ; // avoid multiplying
const leftPadding = self . snapIntoPlatformOverlap ,
rightPadding = self . snapIntoPlatformOverlap ,
topPadding = self . snapIntoPlatformOverlap ,
bottomPadding = self . snapIntoPlatformOverlap ;
const cpos = self . virtualGridToPolygonColliderBLPos ( vx , vy , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) ; // the collider center is kept having integer coords
const pts = [ [ 0 , 0 ] , [ leftPadding + colliderWidth + rightPadding , 0 ] , [ leftPadding + colliderWidth + rightPadding , bottomPadding + colliderHeight + topPadding ] , [ 0 , bottomPadding + colliderHeight + topPadding ] ] ;
2022-09-20 15:50:01 +00:00
2022-12-11 04:20:42 +00:00
// [WARNING] The animNode "anchor & offset" are tuned to fit in this collider by "ControlledCharacter prefab & AttackingCharacter.js"!
2022-12-15 02:03:48 +00:00
const newPlayerCollider = self . collisionSys . createPolygon ( cpos [ 0 ] , cpos [ 1 ] , pts ) ;
2022-09-24 04:01:50 +00:00
const collisionPlayerIndex = self . collisionPlayerIndexPrefix + joinIndex ;
2022-11-23 14:11:28 +00:00
newPlayerCollider . data = playerDownsyncInfo ;
2022-11-11 05:27:48 +00:00
self . collisionSysMap . set ( collisionPlayerIndex , newPlayerCollider ) ;
2022-09-24 04:01:50 +00:00
2022-12-01 03:35:56 +00:00
console . log ( ` Created new player collider: joinIndex= ${ joinIndex } , colliderRadius= ${ playerDownsyncInfo . colliderRadius } ` ) ;
2022-09-24 04:01:50 +00:00
safelyAddChild ( self . node , newPlayerNode ) ;
2022-09-20 15:50:01 +00:00
setLocalZOrder ( newPlayerNode , 5 ) ;
newPlayerNode . active = true ;
2022-11-25 03:20:05 +00:00
playerScriptIns . updateCharacterAnim ( playerDownsyncInfo , null , true ) ;
2022-09-20 15:50:01 +00:00
return [ newPlayerNode , playerScriptIns ] ;
} ,
update ( dt ) {
const self = this ;
2022-09-25 12:48:09 +00:00
if ( ALL _BATTLE _STATES . IN _BATTLE == self . battleState ) {
2022-10-01 15:54:48 +00:00
const elapsedMillisSinceLastFrameIdTriggered = performance . now ( ) - self . lastRenderFrameIdTriggeredAt ;
2022-11-29 13:32:18 +00:00
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!
2022-10-01 15:54:48 +00:00
// console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered);
2022-10-01 12:45:38 +00:00
return ;
}
2022-09-25 12:48:09 +00:00
try {
2022-10-02 16:22:05 +00:00
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 ) ) {
2022-11-25 03:20:05 +00:00
[ prevSelfInput , currSelfInput ] = self . _generateInputFrameUpsync ( noDelayInputFrameId ) ;
2022-10-02 16:22:05 +00:00
}
2022-09-24 04:01:50 +00:00
2022-10-02 16:22:05 +00:00
let t0 = performance . now ( ) ;
if ( self . shouldSendInputFrameUpsyncBatch ( prevSelfInput , currSelfInput , self . lastUpsyncInputFrameId , noDelayInputFrameId ) ) {
// TODO: Is the following statement run asynchronously in an implicit manner? Should I explicitly run it asynchronously?
self . sendInputFrameUpsyncBatch ( noDelayInputFrameId ) ;
}
let t1 = performance . now ( ) ;
// Use "fractional-frame-chasing" to guarantee that "self.update(dt)" is not jammed by a "large range of frame-chasing". See `<proj-root>/ConcerningEdgeCases.md` for the motivation.
const prevChaserRenderFrameId = self . chaserRenderFrameId ;
let nextChaserRenderFrameId = ( prevChaserRenderFrameId + self . maxChasingRenderFramesPerUpdate ) ;
2022-10-03 03:42:19 +00:00
if ( nextChaserRenderFrameId > self . renderFrameId ) {
2022-10-02 16:22:05 +00:00
nextChaserRenderFrameId = self . renderFrameId ;
2022-10-03 03:42:19 +00:00
}
2022-11-11 05:27:48 +00:00
self . rollbackAndChase ( prevChaserRenderFrameId , nextChaserRenderFrameId , self . collisionSys , self . collisionSysMap , true ) ;
2022-10-02 16:22:05 +00:00
let t2 = performance . now ( ) ;
2022-11-11 05:27:48 +00:00
// Inside the following "self.rollbackAndChase" actually ROLLS FORWARD w.r.t. the corresponding delayedInputFrame, REGARDLESS OF whether or not "self.chaserRenderFrameId == self.renderFrameId" now.
2022-11-25 03:20:05 +00:00
const [ prevRdf , rdf ] = self . rollbackAndChase ( self . renderFrameId , self . renderFrameId + 1 , self . collisionSys , self . collisionSysMap , false ) ;
2022-10-10 06:33:04 +00:00
/ *
const nonTrivialChaseEnded = ( prevChaserRenderFrameId < nextChaserRenderFrameId && nextChaserRenderFrameId == self . renderFrameId ) ;
if ( nonTrivialChaseEnded ) {
console . debug ( "Non-trivial chase ended, prevChaserRenderFrameId=" + prevChaserRenderFrameId + ", nextChaserRenderFrameId=" + nextChaserRenderFrameId ) ;
}
* /
2022-11-25 03:20:05 +00:00
// [WARNING] Don't try to get "prevRdf(i.e. renderFrameId == latest-1)" by "self.recentRenderCache.getByFrameId(...)" here, as the cache might have been updated by asynchronous "onRoomDownsyncFrame(...)" calls!
self . applyRoomDownsyncFrameDynamics ( rdf , prevRdf ) ;
2022-12-12 15:17:54 +00:00
self . showDebugBoundaries ( rdf ) ;
2022-11-29 13:32:18 +00:00
++ self . renderFrameId ; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!!
self . lastRenderFrameIdTriggeredAt = performance . now ( ) ;
2022-10-02 16:22:05 +00:00
let t3 = performance . now ( ) ;
2022-09-25 12:48:09 +00:00
} catch ( err ) {
console . error ( "Error during Map.update" , err ) ;
2022-11-29 13:32:18 +00:00
self . onBattleStopped ( ) ; // TODO: Popup to ask player to refresh browser
2022-09-25 12:48:09 +00:00
} finally {
2022-11-25 09:57:10 +00:00
const countdownSeconds = parseInt ( self . countdownNanos / 1000000000 ) ;
if ( isNaN ( countdownSeconds ) ) {
console . warn ( ` countdownSeconds is NaN for countdownNanos == ${ self . countdownNanos } . ` ) ;
}
if ( null != self . countdownLabel ) {
2022-09-25 12:48:09 +00:00
self . countdownLabel . string = countdownSeconds ;
2022-09-20 15:50:01 +00:00
}
}
}
} ,
transitToState ( s ) {
const self = this ;
self . state = s ;
} ,
logout ( byClick /* The case where this param is "true" will be triggered within `ConfirmLogout.js`.*/ , shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage ) {
const self = this ;
const localClearance = ( ) => {
window . clearLocalStorageAndBackToLoginScene ( shouldRetainBoundRoomIdInBothVolatileAndPersistentStorage ) ;
}
const selfPlayerStr = cc . sys . localStorage . getItem ( "selfPlayer" ) ;
if ( null == selfPlayerStr ) {
localClearance ( ) ;
2022-10-02 16:22:05 +00:00
return ;
2022-09-20 15:50:01 +00:00
}
const selfPlayerInfo = JSON . parse ( selfPlayerStr ) ;
try {
NetworkUtils . ajax ( {
url : backendAddress . PROTOCOL + '://' + backendAddress . HOST + ':' + backendAddress . PORT + constants . ROUTE _PATH . API + constants . ROUTE _PATH . PLAYER + constants . ROUTE _PATH . VERSION + constants . ROUTE _PATH . INT _AUTH _TOKEN + constants . ROUTE _PATH . LOGOUT ,
type : "POST" ,
2022-10-02 16:22:05 +00:00
data : {
intAuthToken : selfPlayerInfo . intAuthToken
} ,
2022-09-20 15:50:01 +00:00
success : function ( res ) {
if ( res . ret != constants . RET _CODE . OK ) {
console . log ( "Logout failed: " , res ) ;
}
localClearance ( ) ;
} ,
error : function ( xhr , status , errMsg ) {
localClearance ( ) ;
} ,
timeout : function ( ) {
localClearance ( ) ;
}
} ) ;
} catch ( e ) { } finally {
// For Safari (both desktop and mobile).
localClearance ( ) ;
}
} ,
onLogoutClicked ( evt ) {
const self = this ;
self . showPopupInCanvas ( self . confirmLogoutNode ) ;
} ,
onLogoutConfirmationDismissed ( ) {
const self = this ;
self . transitToState ( ALL _MAP _STATES . VISUAL ) ;
const canvasNode = self . canvasNode ;
canvasNode . removeChild ( self . confirmLogoutNode ) ;
self . enableInputControls ( ) ;
} ,
onGameRule1v1ModeClicked ( evt , cb ) {
const self = this ;
2022-10-02 16:22:05 +00:00
self . battleState = ALL _BATTLE _STATES . WAITING ;
2022-09-20 15:50:01 +00:00
window . initPersistentSessionClient ( self . initAfterWSConnected , null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ ) ;
self . hideGameRuleNode ( ) ;
} ,
showPopupInCanvas ( toShowNode ) {
const self = this ;
self . disableInputControls ( ) ;
self . transitToState ( ALL _MAP _STATES . SHOWING _MODAL _POPUP ) ;
safelyAddChild ( self . widgetsAboveAllNode , toShowNode ) ;
setLocalZOrder ( toShowNode , 10 ) ;
} ,
2022-11-13 03:37:30 +00:00
hideFindingPlayersGUI ( rdf ) {
2022-10-04 03:24:47 +00:00
const self = this ;
if ( null == self . findingPlayerNode . parent ) return ;
self . findingPlayerNode . parent . removeChild ( self . findingPlayerNode ) ;
} ,
2022-11-09 15:46:11 +00:00
onBattleReadyToStart ( rdf ) {
2022-09-20 15:50:01 +00:00
const self = this ;
2022-11-09 15:46:11 +00:00
const players = rdf . players ;
2022-11-21 09:27:32 +00:00
self . _initPlayerRichInfoDict ( players ) ;
2022-11-09 15:46:11 +00:00
// Show the top status indicators for IN_BATTLE
2022-11-19 12:58:07 +00:00
if ( self . playersInfoNode ) {
const playersInfoScriptIns = self . playersInfoNode . getComponent ( "PlayersInfo" ) ;
2022-11-21 09:27:32 +00:00
for ( let i in players ) {
playersInfoScriptIns . updateData ( players [ i ] ) ;
2022-11-19 12:58:07 +00:00
}
2022-11-09 15:46:11 +00:00
}
2022-11-21 09:27:32 +00:00
console . log ( "Calling `onBattleReadyToStart` with:" , players ) ;
2022-11-19 12:58:07 +00:00
if ( self . findingPlayerNode ) {
const findingPlayerScriptIns = self . findingPlayerNode . getComponent ( "FindingPlayer" ) ;
findingPlayerScriptIns . hideExitButton ( ) ;
2022-11-21 09:27:32 +00:00
findingPlayerScriptIns . updatePlayersInfo ( players ) ;
2022-11-19 12:58:07 +00:00
}
2022-09-26 02:36:46 +00:00
2022-10-04 03:24:47 +00:00
// Delay to hide the "finding player" GUI, then show a countdown clock
2022-11-19 12:58:07 +00:00
if ( self . countdownToBeginGameNode ) {
window . setTimeout ( ( ) => {
self . hideFindingPlayersGUI ( ) ;
const countDownScriptIns = self . countdownToBeginGameNode . getComponent ( "CountdownToBeginGame" ) ;
countDownScriptIns . setData ( ) ;
self . showPopupInCanvas ( self . countdownToBeginGameNode ) ;
} , 1500 ) ;
}
2022-09-20 15:50:01 +00:00
} ,
2022-11-25 03:20:05 +00:00
applyRoomDownsyncFrameDynamics ( rdf , prevRdf ) {
2022-09-20 15:50:01 +00:00
const self = this ;
2022-11-25 03:20:05 +00:00
for ( let [ playerId , playerRichInfo ] of self . playerRichInfoDict . entries ( ) ) {
2022-12-14 13:30:01 +00:00
const currPlayerDownsync = rdf . players [ playerId ] ;
2022-11-25 03:20:05 +00:00
const prevRdfPlayer = ( null == prevRdf ? null : prevRdf . players [ playerId ] ) ;
2022-12-14 13:30:01 +00:00
const [ wx , wy ] = self . virtualGridToWorldPos ( currPlayerDownsync . virtualGridX , currPlayerDownsync . virtualGridY ) ;
2022-11-25 03:20:05 +00:00
//const justJiggling = (self.jigglingEps1D >= Math.abs(wx - playerRichInfo.node.x) && self.jigglingEps1D >= Math.abs(wy - playerRichInfo.node.y));
playerRichInfo . node . setPosition ( wx , wy ) ;
2022-12-14 13:30:01 +00:00
playerRichInfo . scriptIns . updateSpeed ( currPlayerDownsync . speed ) ;
playerRichInfo . scriptIns . updateCharacterAnim ( currPlayerDownsync , prevRdfPlayer , false ) ;
2022-11-25 03:20:05 +00:00
}
2022-11-29 13:32:18 +00:00
// Update countdown
self . countdownNanos = self . battleDurationNanos - self . renderFrameId * self . rollbackEstimatedDtNanos ;
if ( self . countdownNanos <= 0 ) {
self . onBattleStopped ( self . playerRichInfoDict ) ;
}
2022-10-02 16:22:05 +00:00
} ,
2022-09-20 15:50:01 +00:00
2022-12-12 15:17:54 +00:00
showDebugBoundaries ( rdf ) {
2022-12-12 11:11:59 +00:00
const self = this ;
2022-12-16 05:55:43 +00:00
const leftPadding = self . snapIntoPlatformOverlap ,
rightPadding = self . snapIntoPlatformOverlap ,
topPadding = self . snapIntoPlatformOverlap ,
bottomPadding = self . snapIntoPlatformOverlap ;
2022-12-12 11:11:59 +00:00
if ( self . showCriticalCoordinateLabels ) {
let g = self . g ;
g . clear ( ) ;
for ( let k in self . collisionSys . _bvh . _bodies ) {
const body = self . collisionSys . _bvh . _bodies [ k ] ;
if ( ! body . _polygon ) continue ;
if ( null != body . data && null != body . data . joinIndex ) {
// character
if ( 1 == body . data . joinIndex ) {
g . strokeColor = cc . Color . BLUE ;
} else {
g . strokeColor = cc . Color . RED ;
}
} else {
// barrier
g . strokeColor = cc . Color . WHITE ;
}
g . moveTo ( body . x , body . y ) ;
const cnt = body . _coords . length ;
for ( let j = 0 ; j < cnt ; j += 2 ) {
const x = body . _coords [ j ] ,
y = body . _coords [ j + 1 ] ;
g . lineTo ( x , y ) ;
}
g . lineTo ( body . x , body . y ) ;
g . stroke ( ) ;
}
// For convenience of recovery upon reconnection, active bullets are always created & immediately removed from "collisionSys" within "applyInputFrameDownsyncDynamicsOnSingleRenderFrame"
for ( let k in rdf . meleeBullets ) {
const meleeBullet = rdf . meleeBullets [ k ] ;
if (
meleeBullet . originatedRenderFrameId + meleeBullet . startupFrames <= rdf . id
&&
meleeBullet . originatedRenderFrameId + meleeBullet . startupFrames + meleeBullet . activeFrames > rdf . id
) {
const offender = rdf . players [ meleeBullet . offenderPlayerId ] ;
if ( 1 == offender . joinIndex ) {
g . strokeColor = cc . Color . BLUE ;
} else {
g . strokeColor = cc . Color . RED ;
}
let xfac = 1 ; // By now, straight Punch offset doesn't respect "y-axis"
if ( 0 > offender . dirX ) {
xfac = - 1 ;
}
const [ offenderWx , offenderWy ] = self . virtualGridToWorldPos ( offender . virtualGridX , offender . virtualGridY ) ;
const bulletWx = offenderWx + xfac * meleeBullet . hitboxOffset ;
2022-12-15 04:28:39 +00:00
const bulletWy = offenderWy ;
2022-12-16 05:55:43 +00:00
const halfColliderWidth = meleeBullet . hitboxSize . x * 0.5 ,
halfColliderHeight = meleeBullet . hitboxSize . y * 0.5 ; // avoid multiplying
const bulletCpos = self . worldToPolygonColliderBLPos ( bulletWx , bulletWy , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) ;
const pts = [ [ 0 , 0 ] , [ leftPadding + meleeBullet . hitboxSize . x + rightPadding , 0 ] , [ leftPadding + meleeBullet . hitboxSize . x + rightPadding , bottomPadding + meleeBullet . hitboxSize . y + topPadding ] , [ 0 , bottomPadding + meleeBullet . hitboxSize . y + topPadding ] ] ;
2022-12-12 11:11:59 +00:00
2022-12-15 04:28:39 +00:00
g . moveTo ( bulletCpos [ 0 ] , bulletCpos [ 1 ] ) ;
2022-12-12 11:11:59 +00:00
for ( let j = 0 ; j < pts . length ; j += 1 ) {
2022-12-15 04:28:39 +00:00
g . lineTo ( pts [ j ] [ 0 ] + bulletCpos [ 0 ] , pts [ j ] [ 1 ] + bulletCpos [ 1 ] ) ;
2022-12-12 11:11:59 +00:00
}
2022-12-15 04:28:39 +00:00
g . lineTo ( bulletCpos [ 0 ] , bulletCpos [ 1 ] ) ;
2022-12-12 11:11:59 +00:00
g . stroke ( ) ;
}
}
}
} ,
2022-09-24 04:01:50 +00:00
getCachedInputFrameDownsyncWithPrediction ( inputFrameId ) {
2022-09-22 04:45:17 +00:00
const self = this ;
2022-11-29 13:32:18 +00:00
const inputFrameDownsync = self . recentInputCache . getByFrameId ( inputFrameId ) ;
const lastAllConfirmedInputFrame = self . recentInputCache . getByFrameId ( self . lastAllConfirmedInputFrameId ) ;
if ( null != inputFrameDownsync && null != lastAllConfirmedInputFrame && inputFrameId > self . lastAllConfirmedInputFrameId ) {
2022-09-22 04:45:17 +00:00
for ( let i = 0 ; i < inputFrameDownsync . inputList . length ; ++ i ) {
2022-11-29 13:32:18 +00:00
if ( i == ( self . selfPlayerInfo . joinIndex - 1 ) ) continue ;
2022-11-24 11:45:48 +00:00
inputFrameDownsync . inputList [ i ] = ( lastAllConfirmedInputFrame . inputList [ i ] & 15 ) ; // Don't predict attack input!
2022-09-22 04:45:17 +00:00
}
2022-10-02 16:22:05 +00:00
}
2022-09-22 04:45:17 +00:00
return inputFrameDownsync ;
} ,
2022-11-10 16:01:53 +00:00
// TODO: Write unit-test for this function to compare with its backend counter part
applyInputFrameDownsyncDynamicsOnSingleRenderFrame ( delayedInputFrame , currRenderFrame , collisionSys , collisionSysMap ) {
2022-11-11 05:27:48 +00:00
const self = this ;
2022-12-16 05:55:43 +00:00
const leftPadding = self . snapIntoPlatformOverlap ,
rightPadding = self . snapIntoPlatformOverlap ,
topPadding = self . snapIntoPlatformOverlap ,
bottomPadding = self . snapIntoPlatformOverlap ;
2022-11-23 14:11:28 +00:00
const nextRenderFramePlayers = { } ;
2022-11-10 16:01:53 +00:00
for ( let playerId in currRenderFrame . players ) {
const currPlayerDownsync = currRenderFrame . players [ playerId ] ;
nextRenderFramePlayers [ playerId ] = {
id : playerId ,
virtualGridX : currPlayerDownsync . virtualGridX ,
virtualGridY : currPlayerDownsync . virtualGridY ,
2022-11-23 14:11:28 +00:00
dirX : currPlayerDownsync . dirX ,
dirY : currPlayerDownsync . dirY ,
2022-12-12 11:11:59 +00:00
velX : currPlayerDownsync . velX ,
velY : currPlayerDownsync . velY ,
2022-11-23 07:04:32 +00:00
characterState : currPlayerDownsync . characterState ,
2022-12-13 10:10:30 +00:00
inAir : true ,
2022-11-10 16:01:53 +00:00
speed : currPlayerDownsync . speed ,
battleState : currPlayerDownsync . battleState ,
score : currPlayerDownsync . score ,
removed : currPlayerDownsync . removed ,
joinIndex : currPlayerDownsync . joinIndex ,
2022-11-23 07:04:32 +00:00
framesToRecover : ( 0 < currPlayerDownsync . framesToRecover ? currPlayerDownsync . framesToRecover - 1 : 0 ) ,
hp : currPlayerDownsync . hp ,
maxHp : currPlayerDownsync . maxHp ,
2022-11-10 16:01:53 +00:00
} ;
2022-09-26 02:36:46 +00:00
}
2022-10-03 03:42:19 +00:00
2022-11-29 13:32:18 +00:00
const nextRenderFrameMeleeBullets = [ ] ;
2022-12-12 11:11:59 +00:00
const effPushbacks = new Array ( self . playerRichInfoArr . length ) ;
2022-12-12 15:17:54 +00:00
const hardPushbackNorms = new Array ( self . playerRichInfoArr . length ) ;
2022-11-23 14:11:28 +00:00
2022-12-12 15:17:54 +00:00
// 1. Process player inputs
/ *
[ WARNING ] Player input alone WOULD NOT take "characterState" into any "ATK_CHARACTER_STATE_IN_AIR_SET" , only after the calculation of "effPushbacks" do we know exactly whether or not a player is "inAir" , the finalize the transition of "thatPlayerInNextFrame.characterState" .
* /
2022-11-10 16:01:53 +00:00
if ( null != delayedInputFrame ) {
2022-11-23 07:04:32 +00:00
const delayedInputFrameForPrevRenderFrame = self . getCachedInputFrameDownsyncWithPrediction ( self . _convertToInputFrameId ( currRenderFrame . id - 1 , self . inputDelayFrames ) ) ;
2022-11-10 16:01:53 +00:00
const inputList = delayedInputFrame . inputList ;
2022-09-29 04:21:04 +00:00
for ( let j in self . playerRichInfoArr ) {
2022-10-02 16:22:05 +00:00
const joinIndex = parseInt ( j ) + 1 ;
2022-11-23 07:04:32 +00:00
const playerRichInfo = self . playerRichInfoArr [ j ] ;
const playerId = playerRichInfo . id ;
2022-12-15 04:28:39 +00:00
const currPlayerDownsync = currRenderFrame . players [ playerId ] ;
2022-12-15 02:03:48 +00:00
const thatPlayerInNextFrame = nextRenderFramePlayers [ playerId ] ;
2022-11-23 14:11:28 +00:00
if ( 0 < thatPlayerInNextFrame . framesToRecover ) {
2022-11-23 07:04:32 +00:00
// No need to process inputs for this player, but there might be bullet pushbacks on this player
continue ;
}
2022-11-09 15:53:45 +00:00
2022-11-20 10:53:33 +00:00
const decodedInput = self . ctrl . decodeInput ( inputList [ joinIndex - 1 ] ) ;
2022-11-23 07:04:32 +00:00
const prevDecodedInput = ( null == delayedInputFrameForPrevRenderFrame ? null : self . ctrl . decodeInput ( delayedInputFrameForPrevRenderFrame . inputList [ joinIndex - 1 ] ) ) ;
const prevBtnALevel = ( null == prevDecodedInput ? 0 : prevDecodedInput . btnALevel ) ;
2022-12-12 11:11:59 +00:00
const prevBtnBLevel = ( null == prevDecodedInput ? 0 : prevDecodedInput . btnBLevel ) ;
if ( 1 == decodedInput . btnBLevel && 0 == prevBtnBLevel ) {
const characStateAlreadyInAir = window . ATK _CHARACTER _STATE _IN _AIR _SET . has ( thatPlayerInNextFrame . characterState ) ;
const characStateIsInterruptWaivable = window . ATK _CHARACTER _STATE _INTERRUPT _WAIVE _SET . has ( thatPlayerInNextFrame . characterState ) ;
if (
! characStateAlreadyInAir
&&
characStateIsInterruptWaivable
) {
thatPlayerInNextFrame . velY = self . jumpingInitVelY ;
2022-12-14 13:30:01 +00:00
if ( 1 == joinIndex ) {
console . log ( ` playerId= ${ playerId } , joinIndex= ${ joinIndex } jumped at {renderFrame.id: ${ currRenderFrame . id } , virtualX: ${ currPlayerDownsync . virtualGridX } , virtualY: ${ currPlayerDownsync . virtualGridY } , nextVelX: ${ thatPlayerInNextFrame . velX } , nextVelY: ${ thatPlayerInNextFrame . velY } }, delayedInputFrame.id= ${ delayedInputFrame . inputFrameId } ` ) ;
}
2022-12-12 11:11:59 +00:00
}
}
2022-11-23 07:04:32 +00:00
if ( 1 == decodedInput . btnALevel && 0 == prevBtnALevel ) {
2022-12-13 10:10:30 +00:00
const punchSkillId = 1 ;
const punch = window . pb . protos . MeleeBullet . create ( self . meleeSkillConfig [ punchSkillId ] ) ;
thatPlayerInNextFrame . framesToRecover = punch . recoveryFrames ;
punch . battleLocalId = self . bulletBattleLocalIdCounter ++ ;
punch . offenderJoinIndex = joinIndex ;
punch . offenderPlayerId = playerId ;
punch . originatedRenderFrameId = currRenderFrame . id ;
nextRenderFrameMeleeBullets . push ( punch ) ;
// console.log(`playerId=${playerId}, joinIndex=${joinIndex} triggered a rising-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`);
thatPlayerInNextFrame . characterState = window . ATK _CHARACTER _STATE . Atk1 [ 0 ] ;
if ( false == currPlayerDownsync . inAir ) {
thatPlayerInNextFrame . velX = 0 ; // prohibits simultaneous movement with Atk1 on the ground
2022-11-23 14:11:28 +00:00
}
2022-11-23 07:04:32 +00:00
} else if ( 0 == decodedInput . btnALevel && 1 == prevBtnALevel ) {
2022-11-23 14:11:28 +00:00
// console.log(`playerId=${playerId} triggered a falling-edge of btnA at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`);
2022-11-23 07:04:32 +00:00
} else {
2022-12-12 15:17:54 +00:00
// No bullet trigger, process joystick movement inputs.
2022-11-23 07:04:32 +00:00
if ( 0 != decodedInput . dx || 0 != decodedInput . dy ) {
// Update directions and thus would eventually update moving animation accordingly
2022-11-23 14:11:28 +00:00
thatPlayerInNextFrame . dirX = decodedInput . dx ;
thatPlayerInNextFrame . dirY = decodedInput . dy ;
2022-12-12 11:11:59 +00:00
thatPlayerInNextFrame . velX = decodedInput . dx * currPlayerDownsync . speed ;
2022-11-23 14:11:28 +00:00
thatPlayerInNextFrame . characterState = window . ATK _CHARACTER _STATE . Walking [ 0 ] ;
2022-11-23 07:04:32 +00:00
} else {
2022-11-23 14:11:28 +00:00
thatPlayerInNextFrame . characterState = window . ATK _CHARACTER _STATE . Idle1 [ 0 ] ;
2022-12-12 11:11:59 +00:00
thatPlayerInNextFrame . velX = 0 ;
2022-11-23 07:04:32 +00:00
}
2022-12-12 11:11:59 +00:00
}
2022-12-12 15:17:54 +00:00
}
}
// 2. Process player movement
for ( let j in self . playerRichInfoArr ) {
const joinIndex = parseInt ( j ) + 1 ;
effPushbacks [ joinIndex - 1 ] = [ 0.0 , 0.0 ] ;
const playerRichInfo = self . playerRichInfoArr [ j ] ;
const playerId = playerRichInfo . id ;
const collisionPlayerIndex = self . collisionPlayerIndexPrefix + joinIndex ;
const playerCollider = collisionSysMap . get ( collisionPlayerIndex ) ;
2022-12-15 02:03:48 +00:00
const currPlayerDownsync = currRenderFrame . players [ playerId ] ;
const thatPlayerInNextFrame = nextRenderFramePlayers [ playerId ] ;
2022-12-12 15:17:54 +00:00
// Reset playerCollider position from the "virtual grid position"
2022-12-16 05:55:43 +00:00
const newVpos = [ currPlayerDownsync . virtualGridX + currPlayerDownsync . velX , currPlayerDownsync . virtualGridY + currPlayerDownsync . velY ] ;
if ( thatPlayerInNextFrame . velY == self . jumpingInitVelY ) {
// This step can be waived, but putting the jumping inclination here makes it easier to read logs.
newVpos [ 1 ] += self . jumpingInitVelY ;
}
const halfColliderWidth = self . playerRichInfoArr [ j ] . colliderRadius ,
halfColliderHeight = self . playerRichInfoArr [ j ] . colliderRadius + self . playerRichInfoArr [ j ] . colliderRadius ; // avoid multiplying
const newCpos = self . virtualGridToPolygonColliderBLPos ( newVpos [ 0 ] , newVpos [ 1 ] , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) ;
2022-12-15 02:03:48 +00:00
playerCollider . x = newCpos [ 0 ] ;
2022-12-15 04:28:39 +00:00
playerCollider . y = newCpos [ 1 ] ;
2022-12-12 15:17:54 +00:00
if ( currPlayerDownsync . inAir ) {
thatPlayerInNextFrame . velX += self . gravityX ;
thatPlayerInNextFrame . velY += self . gravityY ;
}
}
// 3. Add bullet colliders into collision system
const bulletColliders = new Map ( ) ; // Will all be removed at the end of `applyInputFrameDownsyncDynamicsOnSingleRenderFrame` due to the need for being rollback-compatible
const removedBulletsAtCurrFrame = new Set ( ) ;
for ( let k in currRenderFrame . meleeBullets ) {
const meleeBullet = currRenderFrame . meleeBullets [ k ] ;
if (
meleeBullet . originatedRenderFrameId + meleeBullet . startupFrames <= currRenderFrame . id
&&
meleeBullet . originatedRenderFrameId + meleeBullet . startupFrames + meleeBullet . activeFrames > currRenderFrame . id
) {
const collisionBulletIndex = self . collisionBulletIndexPrefix + meleeBullet . battleLocalId ;
const collisionOffenderIndex = self . collisionPlayerIndexPrefix + meleeBullet . offenderJoinIndex ;
const offenderCollider = collisionSysMap . get ( collisionOffenderIndex ) ;
const offender = currRenderFrame . players [ meleeBullet . offenderPlayerId ] ;
let xfac = 1 ; // By now, straight Punch offset doesn't respect "y-axis"
if ( 0 > offender . dirX ) {
xfac = - 1 ;
2022-11-21 09:27:32 +00:00
}
2022-12-12 15:17:54 +00:00
const [ offenderWx , offenderWy ] = self . virtualGridToWorldPos ( offender . virtualGridX , offender . virtualGridY ) ;
const bulletWx = offenderWx + xfac * meleeBullet . hitboxOffset ;
2022-12-15 04:28:39 +00:00
const bulletWy = offenderWy ;
2022-12-16 05:55:43 +00:00
const halfColliderWidth = meleeBullet . hitboxSize . x * 0.5 ,
halfColliderHeight = meleeBullet . hitboxSize . y * 0.5 ;
const bulletCpos = self . worldToPolygonColliderBLPos ( bulletWx , bulletWy , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) ;
const pts = [ [ 0 , 0 ] , [ leftPadding + meleeBullet . hitboxSize . x + rightPadding , 0 ] , [ leftPadding + meleeBullet . hitboxSize . x + rightPadding , bottomPadding + meleeBullet . hitboxSize . y + topPadding ] , [ 0 , bottomPadding + meleeBullet . hitboxSize . y + topPadding ] ] ;
2022-12-15 04:28:39 +00:00
const newBulletCollider = collisionSys . createPolygon ( bulletCpos [ 0 ] , bulletCpos [ 1 ] , pts ) ;
2022-12-12 15:17:54 +00:00
newBulletCollider . data = meleeBullet ;
collisionSysMap . set ( collisionBulletIndex , newBulletCollider ) ;
bulletColliders . set ( collisionBulletIndex , newBulletCollider ) ;
2022-09-29 04:21:04 +00:00
}
2022-12-12 11:11:59 +00:00
}
2022-09-24 04:01:50 +00:00
2022-12-12 15:17:54 +00:00
// 4. Invoke collision system stepping
collisionSys . update ( ) ;
const result = collisionSys . createResult ( ) ; // Can I reuse a "self.collisionSysResult" object throughout the whole battle?
2022-10-02 16:22:05 +00:00
2022-12-12 15:17:54 +00:00
// 5. Calc pushbacks for each player (after its movement) w/o bullets
2022-12-12 11:11:59 +00:00
for ( let j in self . playerRichInfoArr ) {
const joinIndex = parseInt ( j ) + 1 ;
2022-12-12 15:17:54 +00:00
const playerRichInfo = self . playerRichInfoArr [ j ] ;
const playerId = playerRichInfo . id ;
2022-12-12 11:11:59 +00:00
const collisionPlayerIndex = self . collisionPlayerIndexPrefix + joinIndex ;
const playerCollider = collisionSysMap . get ( collisionPlayerIndex ) ;
const potentials = playerCollider . potentials ( ) ;
2022-12-13 10:10:30 +00:00
hardPushbackNorms [ joinIndex - 1 ] = self . calcHardPushbacksNorms ( playerCollider , potentials , result , self . snapIntoPlatformOverlap , effPushbacks [ joinIndex - 1 ] ) ;
2022-12-12 15:17:54 +00:00
2022-12-15 02:03:48 +00:00
const currPlayerDownsync = currRenderFrame . players [ playerId ] ;
const thatPlayerInNextFrame = nextRenderFramePlayers [ playerId ] ;
2022-12-16 05:55:43 +00:00
const halfColliderWidth = self . playerRichInfoArr [ j ] . colliderRadius ,
halfColliderHeight = self . playerRichInfoArr [ j ] . colliderRadius + self . playerRichInfoArr [ j ] . colliderRadius ; // avoid multiplying
2022-12-12 11:11:59 +00:00
let fallStopping = false ;
2022-12-13 10:10:30 +00:00
let possiblyFallStoppedOnAnotherPlayer = false ;
2022-12-12 11:11:59 +00:00
for ( const potential of potentials ) {
2022-12-13 10:10:30 +00:00
let [ isBarrier , isAnotherPlayer , isBullet ] = [ true == potential . data . hardPushback , null != potential . data . joinIndex , null != potential . data . offenderJoinIndex ] ;
2022-12-12 15:17:54 +00:00
// ignore bullets for this step
2022-12-13 10:10:30 +00:00
if ( isBullet ) continue ;
2022-12-12 15:17:54 +00:00
// Test if the player collides with the wall/another player
if ( ! playerCollider . collides ( potential , result ) ) continue ;
const normAlignmentWithGravity = ( result . overlap _x * 0 + result . overlap _y * ( - 1.0 ) ) ;
const landedOnGravityPushback = ( self . snapIntoPlatformThreshold < normAlignmentWithGravity ) ; // prevents false snapping on the lateral sides
2022-12-13 10:10:30 +00:00
let pushback = [ result . overlap * result . overlap _x , result . overlap * result . overlap _y ] ;
2022-12-12 15:17:54 +00:00
if ( landedOnGravityPushback ) {
2022-12-16 05:55:43 +00:00
// kindly note that one player might land on top of another player
2022-12-13 10:10:30 +00:00
pushback = [ ( result . overlap - self . snapIntoPlatformOverlap ) * result . overlap _x , ( result . overlap - self . snapIntoPlatformOverlap ) * result . overlap _y ] ;
thatPlayerInNextFrame . inAir = false ;
}
2022-12-16 05:55:43 +00:00
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 .
* /
pushback = [ ( result . overlap - self . snapIntoPlatformOverlap * 2 ) * result . overlap _x , ( result . overlap - self . snapIntoPlatformOverlap * 2 ) * result . overlap _y ] ; // will overwrite the previous pushback value if "landedOnGravityPushback" is also true
}
2022-12-12 15:17:54 +00:00
for ( let hardPushbackNorm of hardPushbackNorms [ joinIndex - 1 ] ) {
// remove pushback component on the directions of "hardPushbackNorms[joinIndex-1]" (by now those hardPushbacks are already accounted in "effPushbacks[joinIndex-1]")
2022-12-13 10:10:30 +00:00
const projectedMagnitude = pushback [ 0 ] * hardPushbackNorm [ 0 ] + pushback [ 1 ] * hardPushbackNorm [ 1 ] ;
if ( isBarrier
||
( isAnotherPlayer && 0 > projectedMagnitude )
) {
// [WARNING] Pushing by another player is different from pushing by barrier!
// Otherwise the player couldn't be pushed by another player to opposite dir of a side wall
pushback [ 0 ] -= projectedMagnitude * hardPushbackNorm [ 0 ] ;
pushback [ 1 ] -= projectedMagnitude * hardPushbackNorm [ 1 ] ;
}
}
2022-12-14 13:30:01 +00:00
effPushbacks [ joinIndex - 1 ] [ 0 ] += pushback [ 0 ] ;
effPushbacks [ joinIndex - 1 ] [ 1 ] += pushback [ 1 ] ;
2022-12-16 05:55:43 +00:00
// It's not meaningful to log the virtual positions and velocities inside this step.
2022-12-13 10:10:30 +00:00
if ( currPlayerDownsync . inAir && landedOnGravityPushback ) {
fallStopping = true ;
if ( isAnotherPlayer ) {
possiblyFallStoppedOnAnotherPlayer = true ;
}
2022-12-14 13:30:01 +00:00
}
2022-12-16 05:55:43 +00:00
if ( 1 == joinIndex ) {
if ( fallStopping ) {
console . info ( ` playerId= ${ playerId } , joinIndex= ${ thatPlayerInNextFrame . joinIndex } fallStopping#1:
{ renderFrame . id : $ { currRenderFrame . id } , possiblyFallStoppedOnAnotherPlayer : $ { possiblyFallStoppedOnAnotherPlayer } }
playerColliderPos = $ { self . stringifyColliderCenterInWorld ( playerCollider , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) } , effPushback = { $ { effPushbacks [ joinIndex - 1 ] [ 0 ] . toFixed ( 3 ) } , $ { effPushbacks [ joinIndex - 1 ] [ 1 ] . toFixed ( 3 ) } } , overlayMag = $ { result . overlap . toFixed ( 4 ) } ` );
} else if ( currPlayerDownsync . inAir && isBarrier && ! landedOnGravityPushback ) {
console . warn ( ` playerId= ${ playerId } , joinIndex= ${ currPlayerDownsync . joinIndex } inAir & pushed back by barrier & not landed:
{ renderFrame . id : $ { currRenderFrame . id } }
playerColliderPos = $ { self . stringifyColliderCenterInWorld ( playerCollider , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) } , effPushback = { $ { effPushbacks [ joinIndex - 1 ] [ 0 ] . toFixed ( 3 ) } , $ { effPushbacks [ joinIndex - 1 ] [ 1 ] . toFixed ( 3 ) } } , overlayMag = $ { result . overlap . toFixed ( 4 ) } , len ( hardPushbackNorms ) = $ { hardPushbackNorms . length } ` );
} else if ( currPlayerDownsync . inAir && isAnotherPlayer ) {
console . warn ( ` playerId= ${ playerId } , joinIndex= ${ currPlayerDownsync . joinIndex } inAir and pushed back by another player
{ renderFrame . id : $ { currRenderFrame . id } }
playerColliderPos = $ { self . stringifyColliderCenterInWorld ( playerCollider , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) } , anotherPlayerColliderPos = $ { self . stringifyColliderCenterInWorld ( potential , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) } , effPushback = { $ { effPushbacks [ joinIndex - 1 ] [ 0 ] . toFixed ( 3 ) } , $ { effPushbacks [ joinIndex - 1 ] [ 1 ] . toFixed ( 3 ) } } , landedOnGravityPushback = $ { landedOnGravityPushback } , fallStopping = $ { fallStopping } , overlayMag = $ { result . overlap . toFixed ( 4 ) } , len ( hardPushbackNorms ) = $ { hardPushbackNorms . length } ` );
}
2022-12-14 13:30:01 +00:00
}
2022-11-11 05:27:48 +00:00
}
2022-12-12 15:17:54 +00:00
if ( fallStopping ) {
thatPlayerInNextFrame . velX = 0 ;
thatPlayerInNextFrame . velY = 0 ;
thatPlayerInNextFrame . characterState = window . ATK _CHARACTER _STATE . Idle1 [ 0 ] ;
thatPlayerInNextFrame . framesToRecover = 0 ;
}
if ( currPlayerDownsync . inAir ) {
thatPlayerInNextFrame . characterState = window . toInAirConjugate ( thatPlayerInNextFrame . characterState ) ;
}
}
// 6. Check bullet-anything collisions
bulletColliders . forEach ( ( bulletCollider , collisionBulletIndex ) => {
const potentials = bulletCollider . potentials ( ) ;
const offender = currRenderFrame . players [ bulletCollider . data . offenderPlayerId ] ;
let shouldRemove = false ;
for ( const potential of potentials ) {
if ( null != potential . data && potential . data . joinIndex == bulletCollider . data . offenderJoinIndex ) continue ;
if ( ! bulletCollider . collides ( potential , result ) ) continue ;
if ( null != potential . data && null != potential . data . joinIndex ) {
2022-12-13 10:10:30 +00:00
const playerId = potential . data . id ;
2022-12-12 15:17:54 +00:00
const joinIndex = potential . data . joinIndex ;
let xfac = 1 ;
if ( 0 > offender . dirX ) {
xfac = - 1 ;
}
2022-12-13 10:10:30 +00:00
// Only for straight punch, there's no y-pushback
let bulletPushback = [ - xfac * bulletCollider . data . pushback , 0 ] ;
// console.log(`playerId=${playerId}, joinIndex=${joinIndex} is supposed to be pushed back by meleeBullet for bulletPushback=${JSON.stringify(bulletPushback)} at renderFrame.id=${currRenderFrame.id}`);
2022-12-12 15:17:54 +00:00
for ( let hardPushbackNorm of hardPushbackNorms [ joinIndex - 1 ] ) {
2022-12-13 10:10:30 +00:00
const projectedMagnitude = bulletPushback [ 0 ] * hardPushbackNorm [ 0 ] + bulletPushback [ 1 ] * hardPushbackNorm [ 1 ] ;
if ( 0 > projectedMagnitude ) {
// Otherwise when smashing into a wall the atked player would be pushed into the wall first and only got back in the next renderFrame, not what I want here
bulletPushback [ 0 ] -= ( projectedMagnitude * hardPushbackNorm [ 0 ] ) ;
bulletPushback [ 1 ] -= ( projectedMagnitude * hardPushbackNorm [ 1 ] ) ;
2022-12-14 13:30:01 +00:00
// console.log(`playerId=${playerId}, joinIndex=${joinIndex} reducing bulletPushback=${JSON.stringify(bulletPushback)} by ${JSON.stringify([projectedMagnitude * hardPushbackNorm[0], projectedMagnitude * hardPushbackNorm[1]])} where hardPushbackNorm=${JSON.stringify(hardPushbackNorm)}, projectedMagnitude=${projectedMagnitude} at renderFrame.id=${currRenderFrame.id}`);
2022-12-13 10:10:30 +00:00
}
2022-12-12 15:17:54 +00:00
}
2022-12-13 10:10:30 +00:00
// console.log(`playerId=${playerId}, joinIndex=${joinIndex} is actually pushed back by meleeBullet for bulletPushback=${JSON.stringify(bulletPushback)} at renderFrame.id=${currRenderFrame.id}`);
effPushbacks [ joinIndex - 1 ] [ 0 ] += bulletPushback [ 0 ] ;
effPushbacks [ joinIndex - 1 ] [ 1 ] += bulletPushback [ 1 ] ;
2022-12-12 15:17:54 +00:00
const [ atkedPlayerInCurFrame , atkedPlayerInNextFrame ] = [ currRenderFrame . players [ potential . data . id ] , nextRenderFramePlayers [ potential . data . id ] ] ;
atkedPlayerInNextFrame . characterState = window . ATK _CHARACTER _STATE . Atked1 [ 0 ] ;
if ( atkedPlayerInCurFrame . inAir ) {
atkedPlayerInNextFrame . characterState = window . toInAirConjugate ( atkedPlayerInNextFrame . characterState ) ;
}
const oldFramesToRecover = atkedPlayerInNextFrame . framesToRecover ;
atkedPlayerInNextFrame . framesToRecover = ( oldFramesToRecover > bulletCollider . data . hitStunFrames ? oldFramesToRecover : bulletCollider . data . hitStunFrames ) ; // In case the hit player is already stun, we extend it
2022-12-12 11:11:59 +00:00
}
2022-12-12 15:17:54 +00:00
shouldRemove = true ;
}
if ( shouldRemove ) {
removedBulletsAtCurrFrame . add ( collisionBulletIndex ) ;
2022-11-10 16:01:53 +00:00
}
2022-12-12 15:17:54 +00:00
} ) ;
// [WARNING] Remove bullets from collisionSys ANYWAY for the convenience of rollback
for ( let k in currRenderFrame . meleeBullets ) {
const meleeBullet = currRenderFrame . meleeBullets [ k ] ;
const collisionBulletIndex = self . collisionBulletIndexPrefix + meleeBullet . battleLocalId ;
if ( collisionSysMap . has ( collisionBulletIndex ) ) {
const bulletCollider = collisionSysMap . get ( collisionBulletIndex ) ;
bulletCollider . remove ( ) ;
collisionSysMap . delete ( collisionBulletIndex ) ;
}
if ( removedBulletsAtCurrFrame . has ( collisionBulletIndex ) ) continue ;
nextRenderFrameMeleeBullets . push ( meleeBullet ) ;
2022-12-12 11:11:59 +00:00
}
2022-11-23 07:04:32 +00:00
2022-12-12 15:17:54 +00:00
// 7. Get players out of stuck barriers if there's any
2022-12-12 11:11:59 +00:00
for ( let j in self . playerRichInfoArr ) {
const joinIndex = parseInt ( j ) + 1 ;
const playerId = self . playerRichInfoArr [ j ] . id ;
const collisionPlayerIndex = self . collisionPlayerIndexPrefix + joinIndex ;
const playerCollider = collisionSysMap . get ( collisionPlayerIndex ) ;
2022-12-13 10:10:30 +00:00
// Update "virtual grid position"
2022-12-15 02:03:48 +00:00
const currPlayerDownsync = currRenderFrame . players [ playerId ] ;
const thatPlayerInNextFrame = nextRenderFramePlayers [ playerId ] ;
2022-12-16 05:55:43 +00:00
const halfColliderWidth = self . playerRichInfoArr [ j ] . colliderRadius ,
halfColliderHeight = self . playerRichInfoArr [ j ] . colliderRadius + self . playerRichInfoArr [ j ] . colliderRadius ; // avoid multiplying
const newVpos = self . polygonColliderBLToVirtualGridPos ( playerCollider . x - effPushbacks [ joinIndex - 1 ] [ 0 ] , playerCollider . y - effPushbacks [ joinIndex - 1 ] [ 1 ] , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) ;
2022-12-15 02:03:48 +00:00
thatPlayerInNextFrame . virtualGridX = newVpos [ 0 ] ;
thatPlayerInNextFrame . virtualGridY = newVpos [ 1 ] ;
2022-12-14 13:30:01 +00:00
if ( 1 == thatPlayerInNextFrame . joinIndex ) {
if ( currPlayerDownsync . inAir && ! thatPlayerInNextFrame . inAir ) {
2022-12-16 05:55:43 +00:00
console . warn ( ` playerId= ${ playerId } , joinIndex= ${ thatPlayerInNextFrame . joinIndex } fallStopping#2:
{ nextRenderFrame . id : $ { currRenderFrame . id + 1 } , nextVirtualX : $ { thatPlayerInNextFrame . virtualGridX } , nextVirtualY : $ { thatPlayerInNextFrame . virtualGridY } , nextVelX : $ { thatPlayerInNextFrame . velX } , nextVelY : $ { thatPlayerInNextFrame . velY } }
calculated from < - playerColliderPos = $ { self . stringifyColliderCenterInWorld ( playerCollider , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) } , effPushback = { $ { effPushbacks [ joinIndex - 1 ] [ 0 ] . toFixed ( 3 ) } , $ { effPushbacks [ joinIndex - 1 ] [ 1 ] . toFixed ( 3 ) } } ` );
} else if ( ! currPlayerDownsync . inAir && thatPlayerInNextFrame . inAir ) {
console . warn ( ` playerId= ${ playerId } , joinIndex= ${ thatPlayerInNextFrame . joinIndex } took off:
{ nextRenderFrame . id : $ { currRenderFrame . id + 1 } , nextVirtualX : $ { thatPlayerInNextFrame . virtualGridX } , nextVirtualY : $ { thatPlayerInNextFrame . virtualGridY } , nextVelX : $ { thatPlayerInNextFrame . velX } , nextVelY : $ { thatPlayerInNextFrame . velY } }
calculated from < - playerColliderPos = $ { self . stringifyColliderCenterInWorld ( playerCollider , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) } , effPushback = { $ { effPushbacks [ joinIndex - 1 ] [ 0 ] . toFixed ( 3 ) } , $ { effPushbacks [ joinIndex - 1 ] [ 1 ] . toFixed ( 3 ) } } ` );
} else if ( thatPlayerInNextFrame . inAir && 0 != thatPlayerInNextFrame . velY ) {
/ *
console . log ( ` playerId= ${ playerId } , joinIndex= ${ thatPlayerInNextFrame . joinIndex } inAir trajectory:
{ nextRenderFrame . id : $ { currRenderFrame . id + 1 } , nextVirtualX : $ { thatPlayerInNextFrame . virtualGridX } , nextVirtualY : $ { thatPlayerInNextFrame . virtualGridY } , nextVelX : $ { thatPlayerInNextFrame . velX } , nextVelY : $ { thatPlayerInNextFrame . velY } } ;
calculated from < - playerColliderPos = $ { self . stringifyColliderCenterInWorld ( playerCollider , halfColliderWidth , halfColliderHeight , topPadding , bottomPadding , leftPadding , rightPadding ) } , effPushback = { $ { effPushbacks [ joinIndex - 1 ] [ 0 ] . toFixed ( 3 ) } , $ { effPushbacks [ joinIndex - 1 ] [ 1 ] . toFixed ( 3 ) } } ` );
* /
2022-12-14 13:30:01 +00:00
}
}
2022-11-10 16:01:53 +00:00
}
2022-11-29 13:32:18 +00:00
return window . pb . protos . RoomDownsyncFrame . create ( {
id : currRenderFrame . id + 1 ,
players : nextRenderFramePlayers ,
meleeBullets : nextRenderFrameMeleeBullets ,
} ) ;
2022-11-10 16:01:53 +00:00
} ,
2022-11-11 05:27:48 +00:00
rollbackAndChase ( renderFrameIdSt , renderFrameIdEd , collisionSys , collisionSysMap , isChasing ) {
2022-11-10 16:01:53 +00:00
/ *
2022-11-11 05:27:48 +00:00
This function eventually calculates a "RoomDownsyncFrame" where "RoomDownsyncFrame.id == renderFrameIdEd" if not interruptted .
2022-11-10 16:01:53 +00:00
* /
const self = this ;
2022-12-14 13:30:01 +00:00
let prevLatestRdf = null ,
2022-11-29 13:32:18 +00:00
latestRdf = null ;
2022-11-10 16:01:53 +00:00
2022-12-14 13:30:01 +00:00
for ( let i = renderFrameIdSt ; i < renderFrameIdEd ; i ++ ) {
2022-11-29 13:32:18 +00:00
latestRdf = self . recentRenderCache . getByFrameId ( i ) ; // typed "RoomDownsyncFrame"; [WARNING] When "true == isChasing", this function can be interruptted by "onRoomDownsyncFrame(rdf)" asynchronously anytime, making this line return "null"!
if ( null == latestRdf ) {
2022-11-11 15:43:51 +00:00
console . warn ( ` Couldn't find renderFrame for i= ${ i } to rollback, self.renderFrameId= ${ self . renderFrameId } , lastAllConfirmedRenderFrameId= ${ self . lastAllConfirmedRenderFrameId } , lastAllConfirmedInputFrameId= ${ self . lastAllConfirmedInputFrameId } , might've been interruptted by onRoomDownsyncFrame ` ) ;
2022-11-25 03:20:05 +00:00
return [ prevLatestRdf , latestRdf ] ;
2022-11-10 16:01:53 +00:00
}
const j = self . _convertToInputFrameId ( i , self . inputDelayFrames ) ;
const delayedInputFrame = self . getCachedInputFrameDownsyncWithPrediction ( j ) ;
if ( null == delayedInputFrame ) {
2022-11-29 13:32:18 +00:00
// Shouldn't happen!
throw ` Failed to get cached delayedInputFrame for i= ${ i } , j= ${ j } , renderFrameId= ${ self . renderFrameId } , lastAllConfirmedRenderFrameId= ${ self . lastAllConfirmedRenderFrameId } , lastUpsyncInputFrameId= ${ self . lastUpsyncInputFrameId } , lastAllConfirmedInputFrameId= ${ self . lastAllConfirmedInputFrameId } , chaserRenderFrameId= ${ self . chaserRenderFrameId } ; recentRenderCache= ${ self . _stringifyRecentRenderCache ( false ) } , recentInputCache= ${ self . _stringifyRecentInputCache ( false ) } ` ;
2022-09-26 03:16:18 +00:00
}
2022-11-25 03:20:05 +00:00
prevLatestRdf = latestRdf ;
2022-11-29 13:32:18 +00:00
latestRdf = self . applyInputFrameDownsyncDynamicsOnSingleRenderFrame ( delayedInputFrame , prevLatestRdf , collisionSys , collisionSysMap ) ;
2022-11-10 16:01:53 +00:00
if (
self . _allConfirmed ( delayedInputFrame . confirmedList )
&&
latestRdf . id > self . lastAllConfirmedRenderFrameId
) {
// We got a more up-to-date "all-confirmed-render-frame".
self . lastAllConfirmedRenderFrameId = latestRdf . id ;
if ( latestRdf . id > self . chaserRenderFrameId ) {
2022-11-11 05:27:48 +00:00
// it must be true that "chaserRenderFrameId >= lastAllConfirmedRenderFrameId", regardeless of the "isChasing" param
2022-11-10 16:01:53 +00:00
self . chaserRenderFrameId = latestRdf . id ;
}
}
2022-11-11 05:27:48 +00:00
if ( true == isChasing ) {
// Move the cursor "self.chaserRenderFrameId", keep in mind that "self.chaserRenderFrameId" is not monotonic!
self . chaserRenderFrameId = latestRdf . id ;
}
2022-12-01 03:35:56 +00:00
self . recentRenderCache . setByFrameId ( latestRdf , latestRdf . id ) ;
2022-12-14 13:30:01 +00:00
}
2022-10-02 16:22:05 +00:00
2022-11-25 03:20:05 +00:00
return [ prevLatestRdf , latestRdf ] ;
2022-10-02 16:22:05 +00:00
} ,
2022-11-21 09:27:32 +00:00
_initPlayerRichInfoDict ( players ) {
2022-09-20 15:50:01 +00:00
const self = this ;
for ( let k in players ) {
const playerId = parseInt ( k ) ;
2022-09-22 04:45:17 +00:00
if ( self . playerRichInfoDict . has ( playerId ) ) continue ; // Skip already put keys
2022-09-20 15:50:01 +00:00
const immediatePlayerInfo = players [ playerId ] ;
2022-10-02 16:22:05 +00:00
self . playerRichInfoDict . set ( playerId , immediatePlayerInfo ) ;
2022-11-09 15:46:11 +00:00
2022-12-16 05:55:43 +00:00
const nodeAndScriptIns = self . spawnPlayerNode ( immediatePlayerInfo . joinIndex , immediatePlayerInfo . virtualGridX , immediatePlayerInfo . virtualGridY , immediatePlayerInfo ) ;
2022-09-20 15:50:01 +00:00
2022-09-22 04:45:17 +00:00
Object . assign ( self . playerRichInfoDict . get ( playerId ) , {
2022-12-16 05:55:43 +00:00
node : nodeAndScriptIns [ 0 ] ,
scriptIns : nodeAndScriptIns [ 1 ] ,
2022-09-20 15:50:01 +00:00
} ) ;
if ( self . selfPlayerInfo . id == playerId ) {
2022-10-02 16:22:05 +00:00
self . selfPlayerInfo = Object . assign ( self . selfPlayerInfo , immediatePlayerInfo ) ;
2022-12-16 05:55:43 +00:00
nodeAndScriptIns [ 1 ] . showArrowTipNode ( ) ;
2022-09-20 15:50:01 +00:00
}
}
2022-09-26 03:16:18 +00:00
self . playerRichInfoArr = new Array ( self . playerRichInfoDict . size ) ;
self . playerRichInfoDict . forEach ( ( playerRichInfo , playerId ) => {
2022-10-02 16:22:05 +00:00
self . playerRichInfoArr [ playerRichInfo . joinIndex - 1 ] = playerRichInfo ;
} ) ;
2022-09-20 15:50:01 +00:00
} ,
_stringifyRecentInputCache ( usefullOutput ) {
2022-09-26 02:36:46 +00:00
const self = this ;
2022-09-20 15:50:01 +00:00
if ( true == usefullOutput ) {
2022-09-22 09:09:49 +00:00
let s = [ ] ;
2022-09-26 02:36:46 +00:00
for ( let i = self . recentInputCache . stFrameId ; i < self . recentInputCache . edFrameId ; ++ i ) {
s . push ( JSON . stringify ( self . recentInputCache . getByFrameId ( i ) ) ) ;
}
2022-09-22 09:09:49 +00:00
return s . join ( '\n' ) ;
2022-09-20 15:50:01 +00:00
}
2022-11-11 15:43:51 +00:00
return ` [stInputFrameId= ${ self . recentInputCache . stFrameId } , edInputFrameId= ${ self . recentInputCache . edFrameId } ) ` ;
2022-09-20 15:50:01 +00:00
} ,
2022-09-21 09:22:34 +00:00
2022-09-26 02:36:46 +00:00
_stringifyRecentRenderCache ( usefullOutput ) {
2022-09-21 09:22:34 +00:00
const self = this ;
2022-09-26 02:36:46 +00:00
if ( true == usefullOutput ) {
let s = [ ] ;
for ( let i = self . recentRenderCache . stFrameId ; i < self . recentRenderCache . edFrameId ; ++ i ) {
s . push ( JSON . stringify ( self . recentRenderCache . getByFrameId ( i ) ) ) ;
2022-09-21 09:22:34 +00:00
}
2022-09-26 02:36:46 +00:00
return s . join ( '\n' ) ;
}
2022-11-11 15:43:51 +00:00
return ` [stRenderFrameId= ${ self . recentRenderCache . stFrameId } , edRenderFrameId= ${ self . recentRenderCache . edFrameId } ) ` ;
2022-09-21 09:22:34 +00:00
} ,
2022-09-26 02:36:46 +00:00
2022-11-09 10:13:53 +00:00
worldToVirtualGridPos ( x , y ) {
2022-11-10 10:18:00 +00:00
// [WARNING] Introduces loss of precision!
2022-11-09 10:13:53 +00:00
const self = this ;
// In JavaScript floating numbers suffer from seemingly non-deterministic arithmetics, and even if certain libs solved this issue by approaches such as fixed-point-number, they might not be used in other libs -- e.g. the "collision libs" we're interested in -- thus couldn't kill all pains.
2022-11-10 10:18:00 +00:00
let virtualGridX = Math . round ( x * self . worldToVirtualGridRatio ) ;
let virtualGridY = Math . round ( y * self . worldToVirtualGridRatio ) ;
2022-11-09 10:13:53 +00:00
return [ virtualGridX , virtualGridY ] ;
} ,
virtualGridToWorldPos ( vx , vy ) {
2022-11-10 10:18:00 +00:00
// No loss of precision
2022-11-09 10:13:53 +00:00
const self = this ;
2022-12-16 05:55:43 +00:00
return [ vx * self . virtualGridToWorldRatio , vy * self . virtualGridToWorldRatio ] ;
2022-11-09 10:13:53 +00:00
} ,
2022-12-16 05:55:43 +00:00
worldToPolygonColliderBLPos ( wx , wy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding ) {
return [ wx - halfBoundingW - leftPadding , wy - halfBoundingH - bottomPadding ] ;
2022-11-09 10:13:53 +00:00
} ,
2022-12-16 05:55:43 +00:00
polygonColliderBLToWorldPos ( cx , cy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding ) {
return [ cx + halfBoundingW + leftPadding , cy + halfBoundingH + bottomPadding ] ;
2022-11-09 10:13:53 +00:00
} ,
2022-12-16 05:55:43 +00:00
polygonColliderBLToVirtualGridPos ( cx , cy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding ) {
2022-11-09 10:13:53 +00:00
const self = this ;
2022-12-16 05:55:43 +00:00
const [ wx , wy ] = self . polygonColliderBLToWorldPos ( cx , cy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding ) ;
2022-11-25 03:20:05 +00:00
return self . worldToVirtualGridPos ( wx , wy )
2022-11-09 10:13:53 +00:00
} ,
2022-12-16 05:55:43 +00:00
virtualGridToPolygonColliderBLPos ( vx , vy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding ) {
2022-11-09 10:13:53 +00:00
const self = this ;
2022-11-25 03:20:05 +00:00
const [ wx , wy ] = self . virtualGridToWorldPos ( vx , vy ) ;
2022-12-16 05:55:43 +00:00
return self . worldToPolygonColliderBLPos ( wx , wy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding )
} ,
stringifyColliderCenterInWorld ( playerCollider , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding ) {
return ` { ${ ( playerCollider . x + leftPadding + halfBoundingW ) . toFixed ( 2 ) } , ${ ( playerCollider . y + bottomPadding + halfBoundingH ) . toFixed ( 2 ) } } ` ;
2022-11-09 10:13:53 +00:00
} ,
2022-12-12 15:17:54 +00:00
2022-12-13 10:10:30 +00:00
calcHardPushbacksNorms ( collider , potentials , result , snapIntoPlatformOverlap , effPushback ) {
2022-12-14 13:30:01 +00:00
const self = this ;
2022-12-13 10:10:30 +00:00
let ret = [ ] ;
2022-12-12 15:17:54 +00:00
for ( const potential of potentials ) {
if ( null == potential . data || ! ( true == potential . data . hardPushback ) ) continue ;
if ( ! collider . collides ( potential , result ) ) continue ;
2022-12-13 10:10:30 +00:00
// ALWAY snap into hardPushbacks!
// [overlay_x, overlap_y] is the unit vector that points into the platform
2022-12-16 05:55:43 +00:00
const pushback = [ ( result . overlap - snapIntoPlatformOverlap ) * result . overlap _x , ( result . overlap - snapIntoPlatformOverlap ) * result . overlap _y ] ;
2022-12-13 10:10:30 +00:00
ret . push ( [ result . overlap _x , result . overlap _y ] ) ;
2022-12-16 05:55:43 +00:00
effPushback [ 0 ] += pushback [ 0 ] ;
effPushback [ 1 ] += pushback [ 1 ] ;
2022-12-12 15:17:54 +00:00
}
2022-12-13 10:10:30 +00:00
return ret ;
2022-12-12 15:17:54 +00:00
} ,
2022-09-20 15:50:01 +00:00
} ) ;