DelayNoMore/jsexport/models/battle.go

281 lines
13 KiB
Go
Raw Normal View History

2022-12-23 14:31:55 +00:00
package models
import (
"github.com/solarlune/resolv"
. "dnmshared/sharedprotos"
. "jsexport/protos"
. "dnmshared"
)
const (
COLLISION_PLAYER_INDEX_PREFIX = (1 << 17)
COLLISION_BARRIER_INDEX_PREFIX = (1 << 16)
COLLISION_BULLET_INDEX_PREFIX = (1 << 15)
)
// 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 (
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 = int32(4)
ATK_CHARACTER_STATE_INAIR_ATK1 = int32(5)
ATK_CHARACTER_STATE_INAIR_ATKED1 = int32(6)
)
func ConvertToInputFrameId(renderFrameId int32, inputDelayFrames int32, inputScaleFrames int32) int32 {
if renderFrameId < inputDelayFrames {
return 0
}
return ((renderFrameId - inputDelayFrames) >> inputScaleFrames)
}
func DecodeInput(encodedInput uint64) *InputFrameDecoded {
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,
}
}
func CalcHardPushbacksNorms(playerCollider *resolv.Object, playerShape *resolv.ConvexPolygon, snapIntoPlatformOverlap float64, pEffPushback *Vec2D) []Vec2D {
ret := make([]Vec2D, 0, 10) // no one would simultaneously have more than 5 hardPushbacks
collision := playerCollider.Check(0, 0)
if nil == collision {
return ret
}
for _, obj := range collision.Objects {
switch obj.Data.(type) {
case *Barrier:
barrierShape := obj.Shape.(*resolv.ConvexPolygon)
overlapped, pushbackX, pushbackY, overlapResult := CalcPushbacks(0, 0, playerShape, barrierShape)
if !overlapped {
continue
}
// 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
default:
}
}
return ret
}
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame *InputFrameDownsync, currRenderFrame *RoomDownsyncFrame, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, gravityX, gravityY, jumpingInitVelY, inputDelayFrames, inputScaleFrames int32, inputsBuffer *RingBuffer, collisionSpaceOffsetX, collisionSpaceOffsetY, snapIntoPlatformOverlap, snapIntoPlatformThreshold, worldToVirtualGridRatio, virtualGridToWorldRatio float64) *RoomDownsyncFrame {
topPadding, bottomPadding, leftPadding, rightPadding := snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap, snapIntoPlatformOverlap
// [WARNING] This function MUST BE called while "InputsBufferLock" is locked!
roomCapacity := len(currRenderFrame.PlayersArr)
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,
FramesToRecover: currPlayerDownsync.FramesToRecover - 1,
Hp: currPlayerDownsync.Hp,
MaxHp: currPlayerDownsync.MaxHp,
}
if nextRenderFramePlayers[i].FramesToRecover < 0 {
nextRenderFramePlayers[i].FramesToRecover = 0
}
}
effPushbacks := make([]Vec2D, roomCapacity)
hardPushbackNorms := make([][]Vec2D, roomCapacity)
// 1. Process player inputs
if nil != delayedInputFrame {
var delayedInputFrameForPrevRenderFrame *InputFrameDownsync = nil
tmp := inputsBuffer.GetByFrameId(ConvertToInputFrameId(currRenderFrame.Id-1, inputDelayFrames, inputScaleFrames))
if nil != tmp {
delayedInputFrameForPrevRenderFrame = tmp.(*InputFrameDownsync)
}
inputList := delayedInputFrame.InputList
for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
joinIndex := currPlayerDownsync.JoinIndex
thatPlayerInNextFrame := nextRenderFramePlayers[i]
if 0 < thatPlayerInNextFrame.FramesToRecover {
continue
}
decodedInput := DecodeInput(inputList[joinIndex-1])
prevBtnBLevel := int32(0)
if nil != delayedInputFrameForPrevRenderFrame {
prevDecodedInput := DecodeInput(delayedInputFrameForPrevRenderFrame.InputList[joinIndex-1])
prevBtnBLevel = prevDecodedInput.BtnBLevel
}
if decodedInput.BtnBLevel > prevBtnBLevel {
characStateAlreadyInAir := false
if ATK_CHARACTER_STATE_INAIR_IDLE1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_ATK1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_ATKED1 == thatPlayerInNextFrame.CharacterState {
characStateAlreadyInAir = true
}
characStateIsInterruptWaivable := false
if ATK_CHARACTER_STATE_IDLE1 == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_WALKING == thatPlayerInNextFrame.CharacterState || ATK_CHARACTER_STATE_INAIR_IDLE1 == thatPlayerInNextFrame.CharacterState {
characStateIsInterruptWaivable = true
}
if !characStateAlreadyInAir && characStateIsInterruptWaivable {
thatPlayerInNextFrame.VelY = jumpingInitVelY
}
}
// Note that by now "0 == thatPlayerInNextFrame.FramesToRecover", we should change "CharacterState" to "WALKING" or "IDLE" depending on player inputs
if 0 != decodedInput.Dx || 0 != decodedInput.Dy {
thatPlayerInNextFrame.DirX = decodedInput.Dx
thatPlayerInNextFrame.DirY = decodedInput.Dy
thatPlayerInNextFrame.VelX = decodedInput.Dx * currPlayerDownsync.Speed
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_WALKING
} else {
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
thatPlayerInNextFrame.VelX = 0
}
}
}
// 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]
// Reset playerCollider position from the "virtual grid position"
newVx, newVy := currPlayerDownsync.VirtualGridX+currPlayerDownsync.VelX, currPlayerDownsync.VirtualGridY+currPlayerDownsync.VelY
if thatPlayerInNextFrame.VelY == jumpingInitVelY {
newVy += thatPlayerInNextFrame.VelY
}
halfColliderWidth, halfColliderHeight := currPlayerDownsync.ColliderRadius, currPlayerDownsync.ColliderRadius+currPlayerDownsync.ColliderRadius // avoid multiplying
playerCollider.X, playerCollider.Y = VirtualGridToPolygonColliderBLPos(newVx, newVy, halfColliderWidth, halfColliderHeight, topPadding, bottomPadding, leftPadding, rightPadding, collisionSpaceOffsetX, collisionSpaceOffsetY, virtualGridToWorldRatio)
// Update in the collision system
playerCollider.Update()
if currPlayerDownsync.InAir {
thatPlayerInNextFrame.VelX += gravityX
thatPlayerInNextFrame.VelY += gravityY
}
}
// 3. Invoke collision system stepping (no-op for backend collision lib)
// 4. Calc pushbacks for each player (after its movement) w/o bullets
for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
joinIndex := currPlayerDownsync.JoinIndex
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap[collisionPlayerIndex]
playerShape := playerCollider.Shape.(*resolv.ConvexPolygon)
hardPushbackNorms[joinIndex-1] = CalcHardPushbacksNorms(playerCollider, playerShape, snapIntoPlatformOverlap, &(effPushbacks[joinIndex-1]))
thatPlayerInNextFrame := nextRenderFramePlayers[i]
fallStopping := false
if collision := playerCollider.Check(0, 0); nil != collision {
for _, obj := range collision.Objects {
isBarrier, isAnotherPlayer, isBullet := false, false, false
switch obj.Data.(type) {
case *Barrier:
isBarrier = true
case *PlayerDownsync:
isAnotherPlayer = true
case *MeleeBullet:
isBullet = true
}
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))
landedOnGravityPushback := (snapIntoPlatformThreshold < normAlignmentWithGravity) // prevents false snapping on the lateral sides
if landedOnGravityPushback {
// kindly note that one player might land on top of another player, and snapping is also required in such case
pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap)*overlapResult.OverlapY
thatPlayerInNextFrame.InAir = false
}
if isAnotherPlayer {
// [WARNING] The "zero overlap collision" might be randomly detected/missed on either frontend or backend, to have deterministic result we added paddings to all sides of a playerCollider. As each velocity component of (velX, velY) being a multiple of 0.5 at any renderFrame, each position component of (x, y) can only be a multiple of 0.5 too, thus whenever a 1-dimensional collision happens between players from [player#1: i*0.5, player#2: j*0.5, not collided yet] to [player#1: (i+k)*0.5, player#2: j*0.5, collided], the overlap becomes (i+k-j)*0.5+2*s, and after snapping subtraction the effPushback magnitude for each player is (i+k-j)*0.5, resulting in 0.5-multiples-position for the next renderFrame.
pushbackX, pushbackY = (overlapResult.Overlap-snapIntoPlatformOverlap*2)*overlapResult.OverlapX, (overlapResult.Overlap-snapIntoPlatformOverlap*2)*overlapResult.OverlapY
}
for _, hardPushbackNorm := range hardPushbackNorms[joinIndex-1] {
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
if currPlayerDownsync.InAir && landedOnGravityPushback {
fallStopping = true
}
}
}
if fallStopping {
thatPlayerInNextFrame.VelX = 0
thatPlayerInNextFrame.VelY = 0
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_IDLE1
thatPlayerInNextFrame.FramesToRecover = 0
}
if currPlayerDownsync.InAir {
oldNextCharacterState := thatPlayerInNextFrame.CharacterState
switch oldNextCharacterState {
case ATK_CHARACTER_STATE_IDLE1, ATK_CHARACTER_STATE_WALKING:
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_IDLE1
case ATK_CHARACTER_STATE_ATK1:
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_ATK1
case ATK_CHARACTER_STATE_ATKED1:
thatPlayerInNextFrame.CharacterState = ATK_CHARACTER_STATE_INAIR_ATKED1
}
}
}
// 7. Get players out of stuck barriers if there's any
for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
joinIndex := currPlayerDownsync.JoinIndex
collisionPlayerIndex := COLLISION_PLAYER_INDEX_PREFIX + joinIndex
playerCollider := collisionSysMap[collisionPlayerIndex]
// Update "virtual grid position"
thatPlayerInNextFrame := nextRenderFramePlayers[i]
halfColliderWidth, halfColliderHeight := currPlayerDownsync.ColliderRadius, currPlayerDownsync.ColliderRadius+currPlayerDownsync.ColliderRadius // avoid multiplying
thatPlayerInNextFrame.VirtualGridX, thatPlayerInNextFrame.VirtualGridY = PolygonColliderBLToVirtualGridPos(playerCollider.X-effPushbacks[joinIndex-1].X, playerCollider.Y-effPushbacks[joinIndex-1].Y, halfColliderWidth, halfColliderHeight, topPadding, bottomPadding, leftPadding, rightPadding, collisionSpaceOffsetX, collisionSpaceOffsetY, worldToVirtualGridRatio)
}
return &RoomDownsyncFrame{
Id: currRenderFrame.Id + 1,
PlayersArr: nextRenderFramePlayers,
}
}