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