2022-12-25 06:18:48 +00:00
package battle
2022-12-23 14:31:55 +00:00
import (
2022-12-25 06:18:48 +00:00
"math"
2022-12-25 12:17:22 +00:00
"resolv"
2022-12-23 14:31:55 +00:00
)
const (
2022-12-25 06:18:48 +00:00
MAX_FLOAT64 = 1.7e+308
2022-12-23 14:31:55 +00:00
COLLISION_PLAYER_INDEX_PREFIX = ( 1 << 17 )
COLLISION_BARRIER_INDEX_PREFIX = ( 1 << 16 )
COLLISION_BULLET_INDEX_PREFIX = ( 1 << 15 )
2022-12-28 10:06:05 +00:00
PATTERN_ID_UNABLE_TO_OP = - 2
PATTERN_ID_NO_OP = - 1
2022-12-31 07:47:45 +00:00
WORLD_TO_VIRTUAL_GRID_RATIO = float64 ( 100 )
VIRTUAL_GRID_TO_WORLD_RATIO = float64 ( 1.0 ) / WORLD_TO_VIRTUAL_GRID_RATIO
GRAVITY_X = int32 ( 0 )
GRAVITY_Y = - int32 ( float64 ( 0.5 ) * WORLD_TO_VIRTUAL_GRID_RATIO ) // makes all "playerCollider.Y" a multiple of 0.5 in all cases
2023-01-01 07:43:25 +00:00
INPUT_DELAY_FRAMES = int32 ( 8 ) // in the count of render frames
INPUT_SCALE_FRAMES = uint32 ( 2 ) // inputDelayedAndScaledFrameId = ((originalFrameId - InputDelayFrames) >> InputScaleFrames)
NST_DELAY_FRAMES = int32 ( 16 ) // network-single-trip delay in the count of render frames, proposed to be (InputDelayFrames >> 1) because we expect a round-trip delay to be exactly "InputDelayFrames"
2022-12-31 07:47:45 +00:00
SNAP_INTO_PLATFORM_OVERLAP = float64 ( 0.1 )
SNAP_INTO_PLATFORM_THRESHOLD = float64 ( 0.5 )
2022-12-23 14:31:55 +00:00
)
// These directions are chosen such that when speed is changed to "(speedX+delta, speedY+delta)" for any of them, the direction is unchanged.
var DIRECTION_DECODER = [ ] [ ] int32 {
{ 0 , 0 } ,
{ 0 , + 2 } ,
{ 0 , - 2 } ,
{ + 2 , 0 } ,
{ - 2 , 0 } ,
{ + 1 , + 1 } ,
{ - 1 , - 1 } ,
{ + 1 , - 1 } ,
{ - 1 , + 1 } ,
}
const (
2022-12-31 07:47:45 +00:00
ATK_CHARACTER_STATE_IDLE1 = int32 ( 0 )
ATK_CHARACTER_STATE_WALKING = int32 ( 1 )
ATK_CHARACTER_STATE_ATK1 = int32 ( 2 )
ATK_CHARACTER_STATE_ATKED1 = int32 ( 3 )
ATK_CHARACTER_STATE_INAIR_IDLE1_NO_JUMP = int32 ( 4 )
ATK_CHARACTER_STATE_INAIR_IDLE1_BY_JUMP = int32 ( 5 )
ATK_CHARACTER_STATE_INAIR_ATK1 = int32 ( 6 )
ATK_CHARACTER_STATE_INAIR_ATKED1 = int32 ( 7 )
ATK_CHARACTER_STATE_BLOWN_UP1 = int32 ( 8 )
ATK_CHARACTER_STATE_LAY_DOWN1 = int32 ( 9 )
ATK_CHARACTER_STATE_GET_UP1 = int32 ( 10 )
2022-12-23 14:31:55 +00:00
)
2022-12-31 07:47:45 +00:00
var inAirSet = map [ int32 ] bool {
ATK_CHARACTER_STATE_INAIR_IDLE1_NO_JUMP : true ,
ATK_CHARACTER_STATE_INAIR_IDLE1_BY_JUMP : true ,
ATK_CHARACTER_STATE_INAIR_ATK1 : true ,
ATK_CHARACTER_STATE_INAIR_ATKED1 : true ,
ATK_CHARACTER_STATE_BLOWN_UP1 : true ,
}
2022-12-25 12:17:22 +00:00
func ConvertToInputFrameId ( renderFrameId int32 , inputDelayFrames int32 , inputScaleFrames uint32 ) int32 {
2022-12-23 14:31:55 +00:00
if renderFrameId < inputDelayFrames {
return 0
}
return ( ( renderFrameId - inputDelayFrames ) >> inputScaleFrames )
}
2022-12-25 07:39:30 +00:00
func decodeInput ( encodedInput uint64 ) * InputFrameDecoded {
2022-12-23 14:31:55 +00:00
encodedDirection := ( encodedInput & uint64 ( 15 ) )
btnALevel := int32 ( ( encodedInput >> 4 ) & 1 )
btnBLevel := int32 ( ( encodedInput >> 5 ) & 1 )
return & InputFrameDecoded {
Dx : DIRECTION_DECODER [ encodedDirection ] [ 0 ] ,
Dy : DIRECTION_DECODER [ encodedDirection ] [ 1 ] ,
BtnALevel : btnALevel ,
BtnBLevel : btnBLevel ,
}
}
2022-12-25 06:18:48 +00:00
type SatResult struct {
Overlap float64
OverlapX float64
OverlapY float64
AContainedInB bool
BContainedInA bool
2022-12-25 10:44:29 +00:00
Axis resolv . Vector
2022-12-25 06:18:48 +00:00
}
func CalcPushbacks ( oldDx , oldDy float64 , playerShape , barrierShape * resolv . ConvexPolygon ) ( bool , float64 , float64 , * SatResult ) {
origX , origY := playerShape . Position ( )
defer func ( ) {
playerShape . SetPosition ( origX , origY )
} ( )
playerShape . SetPosition ( origX + oldDx , origY + oldDy )
overlapResult := & SatResult {
Overlap : 0 ,
OverlapX : 0 ,
OverlapY : 0 ,
AContainedInB : true ,
BContainedInA : true ,
2022-12-25 10:44:29 +00:00
Axis : resolv . Vector { 0 , 0 } ,
2022-12-25 06:18:48 +00:00
}
if overlapped := isPolygonPairOverlapped ( playerShape , barrierShape , overlapResult ) ; overlapped {
pushbackX , pushbackY := overlapResult . Overlap * overlapResult . OverlapX , overlapResult . Overlap * overlapResult . OverlapY
return true , pushbackX , pushbackY , overlapResult
} else {
return false , 0 , 0 , overlapResult
}
}
func isPolygonPairOverlapped ( a , b * resolv . ConvexPolygon , result * SatResult ) bool {
aCnt , bCnt := len ( a . Points ) , len ( b . Points )
// Single point case
if 1 == aCnt && 1 == bCnt {
if nil != result {
result . Overlap = 0
}
return a . Points [ 0 ] [ 0 ] == b . Points [ 0 ] [ 0 ] && a . Points [ 0 ] [ 1 ] == b . Points [ 0 ] [ 1 ]
}
if 1 < aCnt {
for _ , axis := range a . SATAxes ( ) {
if isPolygonPairSeparatedByDir ( a , b , axis . Unit ( ) , result ) {
return false
}
}
}
if 1 < bCnt {
for _ , axis := range b . SATAxes ( ) {
if isPolygonPairSeparatedByDir ( a , b , axis . Unit ( ) , result ) {
return false
}
}
}
return true
}
2022-12-25 10:44:29 +00:00
func isPolygonPairSeparatedByDir ( a , b * resolv . ConvexPolygon , e resolv . Vector , result * SatResult ) bool {
2022-12-25 06:18:48 +00:00
/ *
[ WARNING ] This function is deliberately made private , it shouldn ' t be used alone ( i . e . not along the norms of a polygon ) , otherwise the pushbacks calculated would be meaningless .
Consider the following example
a : {
anchor : [ 1337.19 1696.74 ]
points : [ [ 0 0 ] [ 24 0 ] [ 24 24 ] [ 0 24 ] ]
} ,
b : {
anchor : [ 1277.72 1570.56 ]
points : [ [ 642.57 319.16 ] [ 0 319.16 ] [ 5.73 0 ] [ 643.75 0.90 ] ]
}
e = ( - 2.98 , 1.49 ) . Unit ( )
* /
var aStart , aEnd , bStart , bEnd float64 = MAX_FLOAT64 , - MAX_FLOAT64 , MAX_FLOAT64 , - MAX_FLOAT64
for _ , p := range a . Points {
dot := ( p [ 0 ] + a . X ) * e [ 0 ] + ( p [ 1 ] + a . Y ) * e [ 1 ]
if aStart > dot {
aStart = dot
}
if aEnd < dot {
aEnd = dot
}
}
for _ , p := range b . Points {
dot := ( p [ 0 ] + b . X ) * e [ 0 ] + ( p [ 1 ] + b . Y ) * e [ 1 ]
if bStart > dot {
bStart = dot
}
if bEnd < dot {
bEnd = dot
}
}
if aStart > bEnd || aEnd < bStart {
// Separated by unit vector "e"
return true
}
if nil != result {
overlap := float64 ( 0 )
if aStart < bStart {
result . AContainedInB = false
if aEnd < bEnd {
overlap = aEnd - bStart
result . BContainedInA = false
} else {
option1 := aEnd - bStart
option2 := bEnd - aStart
if option1 < option2 {
overlap = option1
} else {
overlap = - option2
}
}
} else {
result . BContainedInA = false
if aEnd > bEnd {
overlap = aStart - bEnd
result . AContainedInB = false
} else {
option1 := aEnd - bStart
option2 := bEnd - aStart
if option1 < option2 {
overlap = option1
} else {
overlap = - option2
}
}
}
currentOverlap := result . Overlap
absoluteOverlap := overlap
if overlap < 0 {
absoluteOverlap = - overlap
}
if ( 0 == result . Axis [ 0 ] && 0 == result . Axis [ 1 ] ) || currentOverlap > absoluteOverlap {
var sign float64 = 1
if overlap < 0 {
sign = - 1
}
result . Overlap = absoluteOverlap
result . OverlapX = e [ 0 ] * sign
result . OverlapY = e [ 1 ] * sign
}
result . Axis = e
}
// the specified unit vector "e" doesn't separate "a" and "b", overlap result is generated
return false
}
2023-01-01 07:43:25 +00:00
func WorldToVirtualGridPos ( wx , wy float64 ) ( int32 , int32 ) {
2022-12-25 06:18:48 +00:00
// [WARNING] Introduces loss of precision!
// 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.
2023-01-01 07:43:25 +00:00
var virtualGridX int32 = int32 ( math . Floor ( wx * WORLD_TO_VIRTUAL_GRID_RATIO ) )
var virtualGridY int32 = int32 ( math . Floor ( wy * WORLD_TO_VIRTUAL_GRID_RATIO ) )
2022-12-25 06:18:48 +00:00
return virtualGridX , virtualGridY
}
2023-01-01 07:43:25 +00:00
func VirtualGridToWorldPos ( vx , vy int32 ) ( float64 , float64 ) {
2022-12-25 06:18:48 +00:00
// No loss of precision
2023-01-01 07:43:25 +00:00
var wx float64 = float64 ( vx ) * VIRTUAL_GRID_TO_WORLD_RATIO
var wy float64 = float64 ( vy ) * VIRTUAL_GRID_TO_WORLD_RATIO
2022-12-25 06:18:48 +00:00
return wx , wy
}
func WorldToPolygonColliderBLPos ( wx , wy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding , collisionSpaceOffsetX , collisionSpaceOffsetY float64 ) ( float64 , float64 ) {
return wx - halfBoundingW - leftPadding + collisionSpaceOffsetX , wy - halfBoundingH - bottomPadding + collisionSpaceOffsetY
}
func PolygonColliderBLToWorldPos ( cx , cy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding , collisionSpaceOffsetX , collisionSpaceOffsetY float64 ) ( float64 , float64 ) {
return cx + halfBoundingW + leftPadding - collisionSpaceOffsetX , cy + halfBoundingH + bottomPadding - collisionSpaceOffsetY
}
2023-01-01 07:43:25 +00:00
func PolygonColliderBLToVirtualGridPos ( cx , cy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding , collisionSpaceOffsetX , collisionSpaceOffsetY float64 ) ( int32 , int32 ) {
2022-12-25 06:18:48 +00:00
wx , wy := PolygonColliderBLToWorldPos ( cx , cy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding , collisionSpaceOffsetX , collisionSpaceOffsetY )
2023-01-01 07:43:25 +00:00
return WorldToVirtualGridPos ( wx , wy )
2022-12-25 06:18:48 +00:00
}
2023-01-01 07:43:25 +00:00
func VirtualGridToPolygonColliderBLPos ( vx , vy int32 , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding , collisionSpaceOffsetX , collisionSpaceOffsetY float64 ) ( float64 , float64 ) {
wx , wy := VirtualGridToWorldPos ( vx , vy )
2022-12-25 06:18:48 +00:00
return WorldToPolygonColliderBLPos ( wx , wy , halfBoundingW , halfBoundingH , topPadding , bottomPadding , leftPadding , rightPadding , collisionSpaceOffsetX , collisionSpaceOffsetY )
}
2022-12-26 14:42:26 +00:00
func calcHardPushbacksNorms ( joinIndex int32 , playerCollider * resolv . Object , playerShape * resolv . ConvexPolygon , snapIntoPlatformOverlap float64 , pEffPushback * Vec2D ) * [ ] Vec2D {
2022-12-23 14:31:55 +00:00
ret := make ( [ ] Vec2D , 0 , 10 ) // no one would simultaneously have more than 5 hardPushbacks
collision := playerCollider . Check ( 0 , 0 )
if nil == collision {
2022-12-26 14:42:26 +00:00
return & ret
2022-12-23 14:31:55 +00:00
}
2022-12-26 14:42:26 +00:00
//playerColliderCenterX, playerColliderCenterY := playerCollider.Center()
//fmt.Printf("joinIndex=%d calcHardPushbacksNorms has non-empty collision;playerColliderPos=(%.2f,%.2f)\n", joinIndex, playerColliderCenterX, playerColliderCenterY)
2022-12-23 14:31:55 +00:00
for _ , obj := range collision . Objects {
2022-12-26 14:42:26 +00:00
isBarrier := false
2022-12-23 14:31:55 +00:00
switch obj . Data . ( type ) {
2022-12-26 14:42:26 +00:00
case * PlayerDownsync :
case * MeleeBullet :
2022-12-23 14:31:55 +00:00
default :
2022-12-26 14:42:26 +00:00
// By default it's a regular barrier, even if data is nil, note that Golang syntax of switch-case is kind of confusing, this "default" condition is met only if "!*PlayerDownsync && !*MeleeBullet".
isBarrier = true
}
if ! isBarrier {
continue
}
barrierShape := obj . Shape . ( * resolv . ConvexPolygon )
overlapped , pushbackX , pushbackY , overlapResult := CalcPushbacks ( 0 , 0 , playerShape , barrierShape )
if ! overlapped {
continue
2022-12-23 14:31:55 +00:00
}
2022-12-26 14:42:26 +00:00
// ALWAY snap into hardPushbacks!
// [OverlapX, OverlapY] is the unit vector that points into the platform
pushbackX , pushbackY = ( overlapResult . Overlap - snapIntoPlatformOverlap ) * overlapResult . OverlapX , ( overlapResult . Overlap - snapIntoPlatformOverlap ) * overlapResult . OverlapY
ret = append ( ret , Vec2D { X : overlapResult . OverlapX , Y : overlapResult . OverlapY } )
pEffPushback . X += pushbackX
pEffPushback . Y += pushbackY
//fmt.Printf("joinIndex=%d calcHardPushbacksNorms found one hardpushback; immediatePushback=(%.2f,%.2f)\n", joinIndex, pushbackX, pushbackY)
2022-12-23 14:31:55 +00:00
}
2022-12-26 14:42:26 +00:00
return & ret
2022-12-23 14:31:55 +00:00
}
2022-12-28 10:06:05 +00:00
func deriveOpPattern ( currPlayerDownsync , thatPlayerInNextFrame * PlayerDownsync , currRenderFrame * RoomDownsyncFrame , inputsBuffer * RingBuffer , inputDelayFrames int32 , inputScaleFrames uint32 ) ( int , bool , int32 , int32 ) {
// returns (patternId, jumpedOrNot, effectiveDx, effectiveDy)
delayedInputFrameId := ConvertToInputFrameId ( currRenderFrame . Id , inputDelayFrames , inputScaleFrames )
delayedInputFrameIdForPrevRdf := ConvertToInputFrameId ( currRenderFrame . Id - 1 , inputDelayFrames , inputScaleFrames )
if 0 >= delayedInputFrameId {
return PATTERN_ID_UNABLE_TO_OP , false , 0 , 0
}
delayedInputList := inputsBuffer . GetByFrameId ( delayedInputFrameId ) . ( * InputFrameDownsync ) . InputList
var delayedInputListForPrevRdf [ ] uint64 = nil
if 0 < delayedInputFrameIdForPrevRdf {
delayedInputListForPrevRdf = inputsBuffer . GetByFrameId ( delayedInputFrameIdForPrevRdf ) . ( * InputFrameDownsync ) . InputList
}
jumpedOrNot := false
joinIndex := currPlayerDownsync . JoinIndex
if 0 < currPlayerDownsync . FramesToRecover {
return PATTERN_ID_UNABLE_TO_OP , false , 0 , 0
}
decodedInput := decodeInput ( delayedInputList [ joinIndex - 1 ] )
effDx , effDy := decodedInput . Dx , decodedInput . Dy
prevBtnALevel , prevBtnBLevel := int32 ( 0 ) , int32 ( 0 )
if nil != delayedInputListForPrevRdf {
prevDecodedInput := decodeInput ( delayedInputListForPrevRdf [ joinIndex - 1 ] )
prevBtnALevel = prevDecodedInput . BtnALevel
prevBtnBLevel = prevDecodedInput . BtnBLevel
}
if decodedInput . BtnBLevel > prevBtnBLevel {
2022-12-31 07:47:45 +00:00
if _ , existent := inAirSet [ currPlayerDownsync . CharacterState ] ; ! existent {
2022-12-28 10:06:05 +00:00
jumpedOrNot = true
}
}
patternId := PATTERN_ID_NO_OP
if decodedInput . BtnALevel > prevBtnALevel {
2023-01-01 07:43:25 +00:00
if currPlayerDownsync . InAir {
patternId = 1
} else {
patternId = 0
}
2022-12-28 10:06:05 +00:00
effDx , effDy = 0 , 0 // Most patterns/skills should not allow simultaneous movement
}
return patternId , jumpedOrNot , effDx , effDy
}
2022-12-25 07:39:30 +00:00
// [WARNING] The params of this method is carefully tuned such that only "battle.RoomDownsyncFrame" is a necessary custom struct.
2023-01-01 07:43:25 +00:00
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame ( inputsBuffer * RingBuffer , currRenderFrame * RoomDownsyncFrame , collisionSys * resolv . Space , collisionSysMap map [ int32 ] * resolv . Object , collisionSpaceOffsetX , collisionSpaceOffsetY float64 , chConfigsOrderedByJoinIndex [ ] * CharacterConfig ) * RoomDownsyncFrame {
2022-12-25 07:39:30 +00:00
// [WARNING] On backend this function MUST BE called while "InputsBufferLock" is locked!
2022-12-24 05:57:32 +00:00
roomCapacity := len ( currRenderFrame . PlayersArr )
2022-12-23 14:31:55 +00:00
nextRenderFramePlayers := make ( [ ] * PlayerDownsync , roomCapacity )
// Make a copy first
for i , currPlayerDownsync := range currRenderFrame . PlayersArr {
nextRenderFramePlayers [ i ] = & PlayerDownsync {
Id : currPlayerDownsync . Id ,
VirtualGridX : currPlayerDownsync . VirtualGridX ,
VirtualGridY : currPlayerDownsync . VirtualGridY ,
DirX : currPlayerDownsync . DirX ,
DirY : currPlayerDownsync . DirY ,
VelX : currPlayerDownsync . VelX ,
VelY : currPlayerDownsync . VelY ,
CharacterState : currPlayerDownsync . CharacterState ,
InAir : true ,
Speed : currPlayerDownsync . Speed ,
BattleState : currPlayerDownsync . BattleState ,
Score : currPlayerDownsync . Score ,
Removed : currPlayerDownsync . Removed ,
JoinIndex : currPlayerDownsync . JoinIndex ,
Hp : currPlayerDownsync . Hp ,
MaxHp : currPlayerDownsync . MaxHp ,
2022-12-31 07:47:45 +00:00
FramesToRecover : currPlayerDownsync . FramesToRecover - 1 ,
FramesInChState : currPlayerDownsync . FramesInChState + 1 ,
2022-12-23 14:31:55 +00:00
}
if nextRenderFramePlayers [ i ] . FramesToRecover < 0 {
nextRenderFramePlayers [ i ] . FramesToRecover = 0
}
}
2022-12-28 10:06:05 +00:00
nextRenderFrameMeleeBullets := make ( [ ] * MeleeBullet , 0 , len ( currRenderFrame . MeleeBullets ) ) // Is there any better way to reduce malloc/free impact, e.g. smart prediction for fixed memory allocation?
2022-12-23 14:31:55 +00:00
effPushbacks := make ( [ ] Vec2D , roomCapacity )
2022-12-26 14:42:26 +00:00
hardPushbackNorms := make ( [ ] * [ ] Vec2D , roomCapacity )
2022-12-31 07:47:45 +00:00
jumpedOrNotList := make ( [ ] bool , roomCapacity )
2022-12-23 14:31:55 +00:00
// 1. Process player inputs
2022-12-28 10:06:05 +00:00
for i , currPlayerDownsync := range currRenderFrame . PlayersArr {
2022-12-31 07:47:45 +00:00
jumpedOrNotList [ i ] = false
2023-01-01 07:43:25 +00:00
chConfig := chConfigsOrderedByJoinIndex [ i ]
2022-12-28 10:06:05 +00:00
thatPlayerInNextFrame := nextRenderFramePlayers [ i ]
2022-12-31 07:47:45 +00:00
patternId , jumpedOrNot , effDx , effDy := deriveOpPattern ( currPlayerDownsync , thatPlayerInNextFrame , currRenderFrame , inputsBuffer , INPUT_DELAY_FRAMES , INPUT_SCALE_FRAMES )
2022-12-28 10:06:05 +00:00
if PATTERN_ID_UNABLE_TO_OP == patternId {
continue
}
2022-12-23 14:31:55 +00:00
2022-12-28 10:06:05 +00:00
if jumpedOrNot {
2023-01-01 07:43:25 +00:00
thatPlayerInNextFrame . VelY = int32 ( chConfig . JumpingInitVelY )
2022-12-31 07:47:45 +00:00
jumpedOrNotList [ i ] = true
2022-12-28 10:06:05 +00:00
}
joinIndex := currPlayerDownsync . JoinIndex
if PATTERN_ID_NO_OP != patternId {
2023-01-01 07:43:25 +00:00
if skillId , existent := chConfig . PatternIdToSkillId [ patternId ] ; existent {
skillConfig := skills [ skillId ]
// Hardcoded to use only the first hit for now
switch v := skillConfig . Hits [ 0 ] . ( type ) {
case * MeleeBullet :
var newBullet MeleeBullet = * v // Copied primitive fields into an onstack variable
newBullet . OriginatedRenderFrameId = currRenderFrame . Id
newBullet . OffenderJoinIndex = joinIndex
nextRenderFrameMeleeBullets = append ( nextRenderFrameMeleeBullets , & newBullet )
thatPlayerInNextFrame . FramesToRecover = skillConfig . RecoveryFrames
}
2022-12-28 10:06:05 +00:00
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_ATK1
if false == currPlayerDownsync . InAir {
thatPlayerInNextFrame . VelX = 0
2022-12-23 14:31:55 +00:00
}
}
2022-12-28 10:06:05 +00:00
continue
}
2022-12-23 14:31:55 +00:00
2022-12-28 10:06:05 +00:00
if 0 != effDx || 0 != effDy {
thatPlayerInNextFrame . DirX , thatPlayerInNextFrame . DirY = effDx , effDy
thatPlayerInNextFrame . VelX = effDx * currPlayerDownsync . Speed
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_WALKING
} else {
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_IDLE1
thatPlayerInNextFrame . VelX = 0
2022-12-23 14:31:55 +00:00
}
}
// 2. Process player movement
for i , currPlayerDownsync := range currRenderFrame . PlayersArr {
joinIndex := currPlayerDownsync . JoinIndex
effPushbacks [ joinIndex - 1 ] . X , effPushbacks [ joinIndex - 1 ] . Y = float64 ( 0 ) , float64 ( 0 )
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap [ collisionPlayerIndex ]
thatPlayerInNextFrame := nextRenderFramePlayers [ i ]
2023-01-01 07:43:25 +00:00
chConfig := chConfigsOrderedByJoinIndex [ i ]
2022-12-23 14:31:55 +00:00
// Reset playerCollider position from the "virtual grid position"
newVx , newVy := currPlayerDownsync . VirtualGridX + currPlayerDownsync . VelX , currPlayerDownsync . VirtualGridY + currPlayerDownsync . VelY
2022-12-31 07:47:45 +00:00
if jumpedOrNotList [ i ] {
2023-01-01 07:43:25 +00:00
newVy += chConfig . JumpingInitVelY // Immediately gets out of any snapping
2022-12-31 07:47:45 +00:00
}
2022-12-23 14:31:55 +00:00
2023-01-01 07:43:25 +00:00
playerCollider . X , playerCollider . Y = VirtualGridToPolygonColliderBLPos ( newVx , newVy , playerCollider . W * 0.5 , playerCollider . H * 0.5 , 0 , 0 , 0 , 0 , collisionSpaceOffsetX , collisionSpaceOffsetY )
2022-12-23 14:31:55 +00:00
// Update in the collision system
playerCollider . Update ( )
if currPlayerDownsync . InAir {
2022-12-31 07:47:45 +00:00
thatPlayerInNextFrame . VelX += GRAVITY_X
thatPlayerInNextFrame . VelY += GRAVITY_Y
2022-12-23 14:31:55 +00:00
}
}
2022-12-28 15:12:02 +00:00
// 3. Add bullet colliders into collision system
bulletColliders := make ( [ ] * resolv . Object , 0 , len ( currRenderFrame . MeleeBullets ) ) // Will all be removed at the end of this function due to the need for being rollback-compatible
for _ , meleeBullet := range currRenderFrame . MeleeBullets {
if ( meleeBullet . OriginatedRenderFrameId + meleeBullet . StartupFrames <= currRenderFrame . Id ) && ( meleeBullet . OriginatedRenderFrameId + meleeBullet . StartupFrames + meleeBullet . ActiveFrames > currRenderFrame . Id ) {
offender := currRenderFrame . PlayersArr [ meleeBullet . OffenderJoinIndex - 1 ]
2023-01-01 07:43:25 +00:00
xfac := int32 ( 1 ) // By now, straight Punch offset doesn't respect "y-axis"
2022-12-28 15:12:02 +00:00
if 0 > offender . DirX {
2023-01-01 07:43:25 +00:00
xfac = - xfac
2022-12-28 15:12:02 +00:00
}
2023-01-01 07:43:25 +00:00
bulletWx , bulletWy := VirtualGridToWorldPos ( offender . VirtualGridX + xfac * meleeBullet . HitboxOffsetX , offender . VirtualGridY )
hitboxSizeWx , hitboxSizeWy := VirtualGridToWorldPos ( meleeBullet . HitboxSizeX , meleeBullet . HitboxSizeY )
newBulletCollider := GenerateRectCollider ( bulletWx , bulletWy , hitboxSizeWx , hitboxSizeWy , SNAP_INTO_PLATFORM_OVERLAP , SNAP_INTO_PLATFORM_OVERLAP , SNAP_INTO_PLATFORM_OVERLAP , SNAP_INTO_PLATFORM_OVERLAP , collisionSpaceOffsetX , collisionSpaceOffsetY , meleeBullet , "MeleeBullet" )
2022-12-28 15:12:02 +00:00
collisionSys . Add ( newBulletCollider )
bulletColliders = append ( bulletColliders , newBulletCollider )
} else {
nextRenderFrameMeleeBullets = append ( nextRenderFrameMeleeBullets , meleeBullet )
}
}
// 4. Calc pushbacks for each player (after its movement) w/o bullets
2022-12-23 14:31:55 +00:00
for i , currPlayerDownsync := range currRenderFrame . PlayersArr {
joinIndex := currPlayerDownsync . JoinIndex
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap [ collisionPlayerIndex ]
playerShape := playerCollider . Shape . ( * resolv . ConvexPolygon )
2022-12-31 07:47:45 +00:00
hardPushbackNorms [ joinIndex - 1 ] = calcHardPushbacksNorms ( joinIndex , playerCollider , playerShape , SNAP_INTO_PLATFORM_OVERLAP , & ( effPushbacks [ joinIndex - 1 ] ) )
2022-12-23 14:31:55 +00:00
thatPlayerInNextFrame := nextRenderFramePlayers [ i ]
2023-01-01 07:43:25 +00:00
chConfig := chConfigsOrderedByJoinIndex [ i ]
2022-12-26 14:42:26 +00:00
landedOnGravityPushback := false
2023-01-01 07:43:25 +00:00
2022-12-23 14:31:55 +00:00
if collision := playerCollider . Check ( 0 , 0 ) ; nil != collision {
for _ , obj := range collision . Objects {
isBarrier , isAnotherPlayer , isBullet := false , false , false
switch obj . Data . ( type ) {
case * PlayerDownsync :
isAnotherPlayer = true
case * MeleeBullet :
isBullet = true
2022-12-25 07:39:30 +00:00
default :
// By default it's a regular barrier, even if data is nil
isBarrier = true
2022-12-23 14:31:55 +00:00
}
if isBullet {
// ignore bullets for this step
continue
}
bShape := obj . Shape . ( * resolv . ConvexPolygon )
overlapped , pushbackX , pushbackY , overlapResult := CalcPushbacks ( 0 , 0 , playerShape , bShape )
if ! overlapped {
continue
}
normAlignmentWithGravity := ( overlapResult . OverlapX * float64 ( 0 ) + overlapResult . OverlapY * float64 ( - 1.0 ) )
if isAnotherPlayer {
2022-12-24 05:57:32 +00:00
// [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.
2022-12-31 07:47:45 +00:00
pushbackX , pushbackY = ( overlapResult . Overlap - SNAP_INTO_PLATFORM_OVERLAP * 2 ) * overlapResult . OverlapX , ( overlapResult . Overlap - SNAP_INTO_PLATFORM_OVERLAP * 2 ) * overlapResult . OverlapY
2022-12-23 14:31:55 +00:00
}
2022-12-26 14:42:26 +00:00
for _ , hardPushbackNorm := range * hardPushbackNorms [ joinIndex - 1 ] {
2022-12-23 14:31:55 +00:00
projectedMagnitude := pushbackX * hardPushbackNorm . X + pushbackY * hardPushbackNorm . Y
if isBarrier || ( isAnotherPlayer && 0 > projectedMagnitude ) {
pushbackX -= projectedMagnitude * hardPushbackNorm . X
pushbackY -= projectedMagnitude * hardPushbackNorm . Y
}
}
effPushbacks [ joinIndex - 1 ] . X += pushbackX
effPushbacks [ joinIndex - 1 ] . Y += pushbackY
2022-12-26 14:42:26 +00:00
2022-12-31 07:47:45 +00:00
if SNAP_INTO_PLATFORM_THRESHOLD < normAlignmentWithGravity {
2022-12-26 14:42:26 +00:00
landedOnGravityPushback = true
//playerColliderCenterX, playerColliderCenterY := playerCollider.Center()
//fmt.Printf("joinIndex=%d landedOnGravityPushback\n{renderFrame.id: %d, isBarrier: %v, isAnotherPlayer: %v}\nhardPushbackNormsOfThisPlayer=%v, playerColliderPos=(%.2f,%.2f), immediatePushback={%.3f, %.3f}, effPushback={%.3f, %.3f}, overlapMag=%.4f\n", joinIndex, currRenderFrame.Id, isBarrier, isAnotherPlayer, *hardPushbackNorms[joinIndex-1], playerColliderCenterX, playerColliderCenterY, pushbackX, pushbackY, effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y, overlapResult.Overlap)
2022-12-23 14:31:55 +00:00
}
}
}
2022-12-26 14:42:26 +00:00
if landedOnGravityPushback {
thatPlayerInNextFrame . InAir = false
2022-12-29 04:36:57 +00:00
if currPlayerDownsync . InAir && 0 >= currPlayerDownsync . VelY {
2022-12-26 14:42:26 +00:00
// fallStopping
thatPlayerInNextFrame . VelX = 0
thatPlayerInNextFrame . VelY = 0
2023-01-01 07:43:25 +00:00
if ATK_CHARACTER_STATE_BLOWN_UP1 == thatPlayerInNextFrame . CharacterState {
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_LAY_DOWN1
thatPlayerInNextFrame . FramesToRecover = chConfig . LayDownFramesToRecover
2022-12-31 07:47:45 +00:00
} else {
2023-01-01 07:43:25 +00:00
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_IDLE1
thatPlayerInNextFrame . FramesToRecover = 0
}
} else {
// not fallStopping, could be in LayDown or GetUp
if ATK_CHARACTER_STATE_LAY_DOWN1 == thatPlayerInNextFrame . CharacterState {
if 0 == thatPlayerInNextFrame . FramesToRecover {
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_GET_UP1
thatPlayerInNextFrame . FramesToRecover = chConfig . GetUpFramesToRecover
}
} else if ATK_CHARACTER_STATE_GET_UP1 == thatPlayerInNextFrame . CharacterState {
if thatPlayerInNextFrame . FramesInChState == chConfig . GetUpFrames {
// [WARNING] Before reaching here, the player had 3 invinsible frames to either attack or jump, if it ever took any action then this condition wouldn't have been met, thus we hereby only transit it back to IDLE as it took no action
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_IDLE1
}
2022-12-31 07:47:45 +00:00
}
2022-12-23 14:31:55 +00:00
}
}
}
2022-12-28 15:12:02 +00:00
// 5. Check bullet-anything collisions
for _ , bulletCollider := range bulletColliders {
collision := bulletCollider . Check ( 0 , 0 )
bulletCollider . Space . Remove ( bulletCollider ) // Make sure that the bulletCollider is always removed for each renderFrame
2023-01-01 07:43:25 +00:00
switch v := bulletCollider . Data . ( type ) {
case * MeleeBullet :
if nil == collision {
nextRenderFrameMeleeBullets = append ( nextRenderFrameMeleeBullets , v )
continue
}
bulletShape := bulletCollider . Shape . ( * resolv . ConvexPolygon )
offender := currRenderFrame . PlayersArr [ v . OffenderJoinIndex - 1 ]
for _ , obj := range collision . Objects {
defenderShape := obj . Shape . ( * resolv . ConvexPolygon )
switch t := obj . Data . ( type ) {
case * PlayerDownsync :
if v . OffenderJoinIndex == t . JoinIndex {
continue
}
overlapped , _ , _ , _ := CalcPushbacks ( 0 , 0 , bulletShape , defenderShape )
if ! overlapped {
continue
}
joinIndex := t . JoinIndex
xfac := int32 ( 1 ) // By now, straight Punch offset doesn't respect "y-axis"
if 0 > offender . DirX {
xfac = - xfac
}
pushbackX , pushbackY := VirtualGridToWorldPos ( - xfac * v . PushbackX , v . PushbackY )
for _ , hardPushbackNorm := range * hardPushbackNorms [ joinIndex - 1 ] {
projectedMagnitude := pushbackX * hardPushbackNorm . X + pushbackY * hardPushbackNorm . Y
if 0 > projectedMagnitude {
//fmt.Printf("defenderPlayerId=%d, joinIndex=%d reducing bullet pushback={%.3f, %.3f} by {%.3f, %.3f} where hardPushbackNorm={%.3f, %.3f}, projectedMagnitude=%.3f at renderFrame.id=%d", t.Id, joinIndex, pushbackX, pushbackY, projectedMagnitude*hardPushbackNorm.X, projectedMagnitude*hardPushbackNorm.Y, hardPushbackNorm.X, hardPushbackNorm.Y, projectedMagnitude, currRenderFrame.Id)
pushbackX -= projectedMagnitude * hardPushbackNorm . X
pushbackY -= projectedMagnitude * hardPushbackNorm . Y
}
2022-12-28 15:12:02 +00:00
}
2023-01-01 07:43:25 +00:00
effPushbacks [ joinIndex - 1 ] . X += pushbackX
effPushbacks [ joinIndex - 1 ] . Y += pushbackY
atkedPlayerInNextFrame := nextRenderFramePlayers [ t . JoinIndex - 1 ]
if v . BlowUp {
atkedPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_BLOWN_UP1
} else {
atkedPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_ATKED1
}
oldFramesToRecover := nextRenderFramePlayers [ t . JoinIndex - 1 ] . FramesToRecover
if v . HitStunFrames > oldFramesToRecover {
atkedPlayerInNextFrame . FramesToRecover = v . HitStunFrames
}
default :
2022-12-28 15:12:02 +00:00
}
}
}
}
// 6. Get players out of stuck barriers if there's any
2022-12-23 14:31:55 +00:00
for i , currPlayerDownsync := range currRenderFrame . PlayersArr {
joinIndex := currPlayerDownsync . JoinIndex
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap [ collisionPlayerIndex ]
// Update "virtual grid position"
thatPlayerInNextFrame := nextRenderFramePlayers [ i ]
2023-01-01 07:43:25 +00:00
thatPlayerInNextFrame . VirtualGridX , thatPlayerInNextFrame . VirtualGridY = PolygonColliderBLToVirtualGridPos ( playerCollider . X - effPushbacks [ joinIndex - 1 ] . X , playerCollider . Y - effPushbacks [ joinIndex - 1 ] . Y , playerCollider . W * 0.5 , playerCollider . H * 0.5 , 0 , 0 , 0 , 0 , collisionSpaceOffsetX , collisionSpaceOffsetY )
// Update "CharacterState"
if thatPlayerInNextFrame . InAir {
oldNextCharacterState := thatPlayerInNextFrame . CharacterState
switch oldNextCharacterState {
case ATK_CHARACTER_STATE_IDLE1 , ATK_CHARACTER_STATE_WALKING :
if jumpedOrNotList [ i ] || ATK_CHARACTER_STATE_INAIR_IDLE1_BY_JUMP == currPlayerDownsync . CharacterState {
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_INAIR_IDLE1_BY_JUMP
} else {
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_INAIR_IDLE1_NO_JUMP
}
case ATK_CHARACTER_STATE_ATK1 :
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_INAIR_ATK1
case ATK_CHARACTER_STATE_ATKED1 :
thatPlayerInNextFrame . CharacterState = ATK_CHARACTER_STATE_INAIR_ATKED1
}
}
2022-12-31 07:47:45 +00:00
if thatPlayerInNextFrame . CharacterState != currPlayerDownsync . CharacterState {
thatPlayerInNextFrame . FramesInChState = 0
}
2022-12-23 14:31:55 +00:00
}
return & RoomDownsyncFrame {
2022-12-28 15:12:02 +00:00
Id : currRenderFrame . Id + 1 ,
PlayersArr : nextRenderFramePlayers ,
MeleeBullets : nextRenderFrameMeleeBullets ,
2022-12-23 14:31:55 +00:00
}
}
2022-12-25 06:18:48 +00:00
func GenerateRectCollider ( wx , wy , w , h , topPadding , bottomPadding , leftPadding , rightPadding , spaceOffsetX , spaceOffsetY float64 , data interface { } , tag string ) * resolv . Object {
blX , blY := WorldToPolygonColliderBLPos ( wx , wy , w * 0.5 , h * 0.5 , topPadding , bottomPadding , leftPadding , rightPadding , spaceOffsetX , spaceOffsetY )
return generateRectColliderInCollisionSpace ( blX , blY , leftPadding + w + rightPadding , bottomPadding + h + topPadding , data , tag )
}
func generateRectColliderInCollisionSpace ( blX , blY , w , h float64 , data interface { } , tag string ) * resolv . Object {
collider := resolv . NewObject ( blX , blY , w , h , tag ) // Unlike its frontend counter part, the position of a "resolv.Object" must be specified by "bottom-left point" because "w" and "h" must be positive, see "resolv.Object.BoundsToSpace" for details
shape := resolv . NewRectangle ( 0 , 0 , w , h )
collider . SetShape ( shape )
collider . Data = data
return collider
}
func GenerateConvexPolygonCollider ( unalignedSrc * Polygon2D , spaceOffsetX , spaceOffsetY float64 , data interface { } , tag string ) * resolv . Object {
aligned := AlignPolygon2DToBoundingBox ( unalignedSrc )
var w , h float64 = 0 , 0
shape := resolv . NewConvexPolygon ( )
for i , pi := range aligned . Points {
for j , pj := range aligned . Points {
if i == j {
continue
}
if math . Abs ( pj . X - pi . X ) > w {
w = math . Abs ( pj . X - pi . X )
}
if math . Abs ( pj . Y - pi . Y ) > h {
h = math . Abs ( pj . Y - pi . Y )
}
}
}
for i := 0 ; i < len ( aligned . Points ) ; i ++ {
p := aligned . Points [ i ]
shape . AddPoints ( p . X , p . Y )
}
collider := resolv . NewObject ( aligned . Anchor . X + spaceOffsetX , aligned . Anchor . Y + spaceOffsetY , w , h , tag )
collider . SetShape ( shape )
collider . Data = data
return collider
}
func AlignPolygon2DToBoundingBox ( input * Polygon2D ) * Polygon2D {
// Transform again to put "anchor" at the "bottom-left point (w.r.t. world space)" of the bounding box for "resolv"
boundingBoxBL := & Vec2D {
X : MAX_FLOAT64 ,
Y : MAX_FLOAT64 ,
}
for _ , p := range input . Points {
if p . X < boundingBoxBL . X {
boundingBoxBL . X = p . X
}
if p . Y < boundingBoxBL . Y {
boundingBoxBL . Y = p . Y
}
}
// Now "input.Anchor" should move to "input.Anchor+boundingBoxBL", thus "boundingBoxBL" is also the value of the negative diff for all "input.Points"
output := & Polygon2D {
Anchor : & Vec2D {
X : input . Anchor . X + boundingBoxBL . X ,
Y : input . Anchor . Y + boundingBoxBL . Y ,
} ,
Points : make ( [ ] * Vec2D , len ( input . Points ) ) ,
}
for i , p := range input . Points {
output . Points [ i ] = & Vec2D {
X : p . X - boundingBoxBL . X ,
Y : p . Y - boundingBoxBL . Y ,
}
}
return output
}