Archived
1
0
Fork 0
This commit is contained in:
Sebastian Benjamin 2025-09-09 16:51:13 -07:00
parent 122590d10b
commit 8fe7bff891
9 changed files with 306 additions and 256 deletions

View file

@ -11,9 +11,9 @@ func _broadcast() -> void:
if !controlled_world:
return
var pos = controlled_world.player.position
var json_string = JSON.stringify({"utctime": Time.get_unix_time_from_system(), "tick": LocalTimer.predicted_tick, "x": pos.x, "y": pos.y})
print("SEND: ", json_string)
var delta_vel = controlled_world.player.get_and_reset_delta_vel()
var json_string = JSON.stringify({"utctime": Time.get_unix_time_from_system(), "tick": LocalTimer.predicted_tick, "vel_x": delta_vel.x, "vel_y": delta_vel.y})
#print("SEND: ", json_string)
network.nakama_socket.send_match_state_async(network.current_match_id, 0, json_string)
func _on_match_state(p_state : NakamaRTAPI.MatchData):
@ -23,10 +23,9 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData):
match p_state.op_code:
2:
var data = JSON.parse_string(p_state.data)
print("RECV: ", data)
# Set player position if server demands a forced position
if data["forcePlayerPos"]:
if true:#data["forcePlayerPos"]:
controlled_world.player.set_position_data(
Vector2(
float(data["playerHitPos"]["x"]),
@ -38,31 +37,31 @@ func _on_match_state(p_state : NakamaRTAPI.MatchData):
controlled_world.ui.text = "Graze: " + str(data["graze"])
# Handle player death if there is an ongoing death timer
if int(data["deathTimer"]) > 0:
controlled_world.player.kill()
elif int(data["deathTimer"]) == 0:
controlled_world.player.resurrect()
#if int(data["deathTimer"]) > 0:
# controlled_world.player.kill()
#elif int(data["deathTimer"]) == 0:
# controlled_world.player.resurrect()
# Spawn new bullets
for b in data["newBullets"]:
var bullet = DanmakuBullet.new()
bullet.setup_bullet(
int(b["class"]),
int(b["tick"]),
b["x"],
b["y"],
b["radius"],
b["vel_x"],
b["vel_y"])
bullet.texture = load("res://test-bullet.png")
bullet.position = bullet.get_current_pos(int(b["tick"]))
# Reimplemented from ScalableSprite2D here atm
var scale_ratio = (b["radius"] * 2) / bullet.texture.get_width()
bullet.scale = Vector2(scale_ratio, scale_ratio)
bullet.z_index = 4
controlled_world.bullet_manager.add_child(bullet)
controlled_world.bullet_manager.bullets.append(bullet)
LocalTimer.predicted_tick = int(b["tick"])
LocalTimer.delta_counter = 0
#for b in data["serverEvents"]:
#var bullet = DanmakuBullet.new()
#bullet.setup_bullet(
#int(b["class"]),
#int(b["tick"]),
#b["x"],
#b["y"],
#b["radius"],
#b["vel_x"],
#b["vel_y"])
#bullet.texture = load("res://test-bullet.png")
#bullet.position = bullet.get_current_pos(int(b["tick"]))
#
## Reimplemented from ScalableSprite2D here atm
#var scale_ratio = (b["radius"] * 2) / bullet.texture.get_width()
#bullet.scale = Vector2(scale_ratio, scale_ratio)
#bullet.z_index = 4
#
#controlled_world.bullet_manager.add_child(bullet)
#controlled_world.bullet_manager.bullets.append(bullet)
#LocalTimer.predicted_tick = int(b["tick"])
#LocalTimer.delta_counter = 0

View file

@ -1,9 +1,11 @@
class_name Player
extends Node2D
@export var speed = 80
@export var controlled = false
var velocity := Vector2.ZERO
const SPEED = 80
var slow = false
var tick_vel := Vector2.ZERO
var delta_vel_since_last_broadcast := Vector2.ZERO
var hurt_collision: DanmakuCircle = DanmakuCircle.new()
var graze_collision: DanmakuCircle = DanmakuCircle.new()
const PLAYER_BODY_WIDTH_MULTIPLIER = 0.18
@ -12,25 +14,30 @@ var alive: bool = true
func _ready() -> void:
$BodySprite.scale_sprite(PLAYER_BODY_WIDTH_MULTIPLIER * Globals.SERVER_SIZE.x)
func get_input():
func get_input(delta):
slow = false
if Input.is_action_pressed("Slow Mode"):
speed = 30
else:
speed = 80
slow = true
velocity = Input.get_vector("Left", "Right", "Up", "Down") * speed
tick_vel = Input.get_vector("Left", "Right", "Up", "Down") * ((SPEED / 3) if slow else SPEED) * delta
delta_vel_since_last_broadcast += tick_vel
func _physics_process(delta: float):
get_input()
get_input(delta)
if !alive:
return
#if !alive:
#return
# Bounds checking
var attempted_position := position + (velocity * delta)
attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE)
#var attempted_position := position + (velocity * delta)
#attempted_position = attempted_position.clamp(Vector2(0, 0), Globals.SERVER_SIZE)
set_position_data(attempted_position, null, null)
#set_position_data(attempted_position, null, null)
func get_and_reset_delta_vel():
var ret = delta_vel_since_last_broadcast
delta_vel_since_last_broadcast = Vector2.ZERO
return ret
func set_position_data(pos: Vector2, hurtcircle_radius, grazecircle_radius):
position = pos
@ -45,7 +52,6 @@ func set_position_data(pos: Vector2, hurtcircle_radius, grazecircle_radius):
graze_collision.set_radius(grazecircle_radius)
$GrazecircleSprite.scale_sprite(grazecircle_radius*2)
func kill():
if alive == false:
return

View file

@ -6,10 +6,17 @@ ENV CGO_ENABLED 1
WORKDIR /backend
COPY . .
RUN go build --trimpath --buildmode=plugin -o ./backend.so
RUN apt-get update && \
apt-get -y upgrade && \
apt-get install -y --no-install-recommends gcc libc6-dev
FROM heroiclabs/nakama:3.26.0
RUN go build --trimpath --gcflags "all=-N -l" --mod=vendor --buildmode=plugin -o ./backend.so
RUN go install github.com/go-delve/delve/cmd/dlv@latest
FROM heroiclabs/nakama-dsym:3.26.0
COPY --from=builder /backend/backend.so /nakama/data/modules
#COPY --from=builder /backend/local.yml /nakama/data/
COPY --from=builder /backend/*.json /nakama/data/modules
ENTRYPOINT [ "/bin/bash" ]

View file

@ -24,7 +24,7 @@ services:
- "-ecx"
- >
/nakama/nakama migrate up --database.address root@cockroachdb:26257 &&
exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level DEBUG --session.token_expiry_sec 7200 --metrics.prometheus_port 9100
exec /nakama/nakama --name nakama1 --database.address root@cockroachdb:26257 --logger.level ERROR --session.token_expiry_sec 7200 --metrics.prometheus_port 9100
restart: "no"
links:
- "cockroachdb:db"

View file

@ -7,6 +7,7 @@ package ffi
#include <stdlib.h>
*/
import "C"
import "strconv"
// Circles
type Circle struct {
@ -19,6 +20,14 @@ func NewCircle(x float64, y float64, rad float64) *Circle {
}
}
func (c *Circle) Clone() *Circle {
if c == nil {
return nil
}
x, y := c.GetPos()
return NewCircle(x, y, float64(c.cptr.radius))
}
func (c *Circle) UpdatePos(x float64, y float64) {
c.cptr.x = C.double(x)
c.cptr.y = C.double(y)
@ -30,6 +39,7 @@ func DestroyCircle(circle *Circle) {
}
C.destroy_circle(circle.cptr)
circle.cptr = nil
}
func (c *Circle) GetPos() (float64, float64) {
@ -39,19 +49,17 @@ func (c *Circle) GetPos() (float64, float64) {
// Bullets
type Bullet struct {
cptr *C.Bullet
DeletionTick int64
}
// Values for selecting bullet paths
const (
BULLET_LINEAR = 0
ACTIVE_BULLET = -1
)
func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64, velX float64, velY float64) *Bullet {
return &Bullet{
cptr: C.new_bullet(
C.uint8_t(0),
C.uint8_t(BULLET_LINEAR),
C.int64_t(tick),
C.double(spawnX),
C.double(spawnY),
@ -59,7 +67,29 @@ func NewLinearBullet(tick int64, spawnX float64, spawnY float64, radius float64,
C.double(velX),
C.double(velY),
),
DeletionTick: ACTIVE_BULLET,
}
}
func (b *Bullet) Clone() *Bullet {
if b == nil {
return nil
}
switch int(b.cptr.class_) {
case BULLET_LINEAR:
nb := NewLinearBullet(
int64(b.cptr.spawn_time),
float64(b.cptr.spawn_x),
float64(b.cptr.spawn_y),
float64(b.cptr.radius),
float64(b.cptr.parameters[0]), // vel_x
float64(b.cptr.parameters[1]), // vel_y
)
return nb
default:
// Mirror other bullet classes here with their exact constructors.
panic("ffi.Bullet.Clone: unsupported bullet class: " + strconv.Itoa(int(b.cptr.class_)))
}
}
@ -69,6 +99,7 @@ func DestroyBullet(bullet *Bullet) {
}
C.destroy_bullet(bullet.cptr)
bullet.cptr = nil
}
func (b *Bullet) BeyondKillBoundary(tick int64) bool {
@ -94,13 +125,17 @@ func (b *Bullet) GetRadius() float64 {
return float64(b.cptr.radius)
}
func (b *Bullet) Serialize(tick int64) map[string]any {
x, y := b.GetPos(tick)
func (b *Bullet) GetSpawnTick() int64 {
return int64(b.cptr.spawn_time)
}
func (b *Bullet) Serialize( /*tick int64*/ ) map[string]any {
//x, y := b.GetPos(tick)
return map[string]any{
"class": b.GetType(),
"tick": tick,
"x": x,
"y": y,
"tick": b.GetSpawnTick(),
//"x": x,
//"y": y,
"radius": b.GetRadius(),
"vel_x": float64(b.cptr.parameters[0]),
"vel_y": float64(b.cptr.parameters[1]),

View file

@ -7,6 +7,18 @@ const (
FINAL_PHASE
MATCH_END
)
const (
LEFT = iota
RIGHT
UP
DOWN
)
const (
ALIVE = iota
DEATH_QUEUED
DEAD
)
const (
TICK_RATE = 60

View file

@ -31,20 +31,22 @@ func StorePlayerInputs(lobbyState *MatchState, messages []runtime.MatchData, log
continue
}
var update ClientUpdate
if err := json.Unmarshal(msg.GetData(), &update); err != nil {
var newInput ClientUpdate
if err := json.Unmarshal(msg.GetData(), &newInput); err != nil {
(*logger).Warn("Failed to parse input: %v", err)
continue
}
// Store the input in the player's stage state
lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, &update)
// Store the input in the player's stage state if it arrived in time
if tick-newInput.Tick < GRACE_WINDOW_TICKS {
lobbyState.presences[msg.GetSessionId()].stageState.playerInputs = append(lobbyState.presences[msg.GetSessionId()].stageState.playerInputs, &newInput)
}
}
}
func BroadcastToPresences(tick int64, lobbyState *MatchState, logger *runtime.Logger, dispatcher *runtime.MatchDispatcher) {
for _, v := range lobbyState.presences {
var tickData = v.stageState.MakeServerTick(tick)
var tickData = v.stageState.SimulateAndMakeServerTick(tick)
data, err := json.Marshal(tickData)
if err != nil {
@ -71,10 +73,7 @@ func (m *Match) MatchLoop(ctx context.Context, logger runtime.Logger, db *sql.DB
StorePlayerInputs(lobbyState, messages, &logger, tick)
for _, playerState := range lobbyState.presences {
playerState.stageState.MarkBulletsBeyondKillBoundary(tick)
playerState.stageState.ProcessPlayerInputs(tick)
playerState.stageState.HandleDeath(tick)
playerState.stageState.CleanupOldBullets(tick)
playerState.stageState = playerState.stageState.ResolveLockedInState(tick)
}
BroadcastToPresences(tick, lobbyState, &logger, &dispatcher)

View file

@ -5,79 +5,17 @@ import (
"danmaku/ffi"
"math"
"slices"
"time"
)
type PlayerStageState struct {
lastLockedInTick int64
hitCol *ffi.Circle
grazeCol *ffi.Circle
bullets []*ffi.Bullet
updatePlayerPos bool
playerInputs []*ClientUpdate
deathState int
health int
graze int
grazePerTick map[int64]int
deathTick int64
dead bool
cancelDeath bool
survivedGraceWindow bool
playerInputs []*ClientUpdate
lastInput *ClientUpdate
}
func (s *PlayerStageState) ProcessPlayerInputs(tick int64) {
// Lock-in the earned graze
cutoffTick := tick - GRACE_WINDOW_TICKS
for t, v := range s.grazePerTick {
if t < cutoffTick {
s.graze += v
delete(s.grazePerTick, t)
}
}
// Clean up inputs outside the grace window
s.playerInputs = slices.DeleteFunc(s.playerInputs, func(input *ClientUpdate) bool {
return tick-input.Tick > GRACE_WINDOW_TICKS
})
// Sort inputs by tick
slices.SortFunc(s.playerInputs, func(a, b *ClientUpdate) int {
return cmp.Compare(a.Tick, b.Tick)
})
// Replay each tick within the grace window
s.survivedGraceWindow = true
for t := tick - GRACE_WINDOW_TICKS; t <= tick; t++ {
// Find the input for the current tick
var currentInput *ClientUpdate
for _, input := range s.playerInputs {
if input.Tick == t {
currentInput = input
break
}
}
// Apply the correct movement or no movement
if currentInput != nil {
s.clampedMove(currentInput.X, currentInput.Y)
s.lastInput = currentInput
} else if s.lastInput != nil {
s.clampedMove(s.lastInput.X, s.lastInput.Y)
}
// If the player dies in the grace window, don't cancel their death
if s.CheckPlayerDeadOnTick(t) {
s.survivedGraceWindow = false
for grazeT := range s.grazePerTick {
if grazeT > t {
delete(s.grazePerTick, grazeT)
}
}
break
} else {
s.grazePerTick[t] = s.CalculateGrazeDelta(t)
}
}
}
func NewPlayerStage() *PlayerStageState {
@ -85,109 +23,168 @@ func NewPlayerStage() *PlayerStageState {
hitCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_HIT_COL_RADIUS_MULTIPLIER),
grazeCol: ffi.NewCircle(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1, STAGE_WIDTH*PLAYER_GRAZE_COL_RADIUS_MULTIPLIER),
bullets: []*ffi.Bullet{},
updatePlayerPos: true,
grazePerTick: make(map[int64]int),
lastLockedInTick: 0,
health: 3,
deathTick: PLAYER_ALIVE,
graze: 0,
}
}
func (s *PlayerStageState) DeepClone() *PlayerStageState {
clone := &PlayerStageState{
lastLockedInTick: s.lastLockedInTick,
health: s.health,
graze: s.graze,
}
// Circles
if s.hitCol != nil {
clone.hitCol = s.hitCol.Clone()
}
if s.grazeCol != nil {
clone.grazeCol = s.grazeCol.Clone()
}
// Bullets
if s.bullets != nil {
clone.bullets = make([]*ffi.Bullet, len(s.bullets))
for i, b := range s.bullets {
clone.bullets[i] = b.Clone()
}
}
// Player inputs
if s.playerInputs != nil {
clone.playerInputs = make([]*ClientUpdate, len(s.playerInputs))
for i, u := range s.playerInputs {
if u == nil {
continue
}
clone.playerInputs[i] = &ClientUpdate{
VelX: u.VelX,
VelY: u.VelY,
Tick: u.Tick,
}
}
}
return clone
}
func (s *PlayerStageState) Delete() {
for _, bullet := range s.bullets {
ffi.DestroyBullet(bullet)
}
s.bullets = nil
ffi.DestroyCircle(s.hitCol)
ffi.DestroyCircle(s.grazeCol)
s.hitCol = nil
s.grazeCol = nil
}
func (s *PlayerStageState) clampedMove(x, y float64) bool {
clampedX := x < 0 || x > STAGE_WIDTH
clampedY := y < 0 || y > STAGE_HEIGHT
func (s *PlayerStageState) ResolveLockedInState(tick int64) *PlayerStageState {
// Rotate the player's input queue and lock in the results of the oldest inputs if they're outside the grace window
// Sort inputs by tick
slices.SortFunc(s.playerInputs, func(a, b *ClientUpdate) int {
return cmp.Compare(a.Tick, b.Tick)
})
// Clean up inputs outside the grace window and store them in a separate list
var lockedInInputs, inputQueue []*ClientUpdate
for _, input := range s.playerInputs {
if tick-input.Tick < GRACE_WINDOW_TICKS {
inputQueue = append(inputQueue, input)
} else {
lockedInInputs = append(lockedInInputs, input)
}
}
newLockedInState := s.NewStateFromInputs(lockedInInputs, tick-GRACE_WINDOW_TICKS)
newLockedInState.playerInputs = inputQueue
s.Delete()
return newLockedInState
}
func (s *PlayerStageState) NewStateFromInputs(inputs []*ClientUpdate, tickToSimulateTo int64) *PlayerStageState {
stageStateCopy := s.DeepClone()
// Replay each tick between the last locked in tick and the tick to simulate to
for t := stageStateCopy.lastLockedInTick; t < tickToSimulateTo; t++ {
// Handle inputs for the current tick on the client
for _, input := range inputs {
if input.Tick == t {
stageStateCopy.ApplyUpdate(input)
}
}
// Filter bullets in place
stageStateCopy.bullets = slices.DeleteFunc(stageStateCopy.bullets, func(b *ffi.Bullet) bool {
if b.BeyondKillBoundary(t) {
ffi.DestroyBullet(b)
return true // remove from slice
}
// Otherwise, handle collisions
if b.CollidesWith(stageStateCopy.hitCol, t) && stageStateCopy.deathState == ALIVE {
// TODO fix whatever this weird sliding issue is on the client
// TODO handle the issue of the transient state being used to compute bullets
// i.e. where do those bullets go? in the past? in the future?
// TODO bullets are impacting the past
// TODO Then replay, since bullets should be working again
// TODO: Finally, handle death and revival using deathState
ffi.DestroyBullet(b)
return true
}
if b.CollidesWith(stageStateCopy.grazeCol, t) && stageStateCopy.deathState == ALIVE {
stageStateCopy.graze += GRAZE_ADDITION_MULTIPLIER
}
return false // keep bullet
})
// Handle server events
stageStateCopy.MakeServerEvents(t)
}
stageStateCopy.lastLockedInTick = tickToSimulateTo
return stageStateCopy
}
func (s *PlayerStageState) ApplyUpdate(update *ClientUpdate) {
dx := update.VelX
dy := update.VelY
if dx == 0 && dy == 0 {
return
}
// clamp max movement distance to 100 units
if dist := math.Hypot(dx, dy); dist > 100 {
scale := 100 / dist
dx *= scale
dy *= scale
}
s.clampedMoveDelta(dx, dy)
}
// Move by a delta
func (s *PlayerStageState) clampedMoveDelta(dx, dy float64) bool {
posX, posY := s.hitCol.GetPos()
x := posX + dx
y := posY + dy
newX := math.Max(0, math.Min(x, STAGE_WIDTH))
newY := math.Max(0, math.Min(y, STAGE_HEIGHT))
clamped := (newX != x) || (newY != y)
s.hitCol.UpdatePos(newX, newY)
s.grazeCol.UpdatePos(newX, newY)
return clampedX || clampedY
}
func (s *PlayerStageState) MarkBulletsBeyondKillBoundary(tick int64) {
for _, b := range s.bullets {
if b.BeyondKillBoundary(tick) && b.DeletionTick == ACTIVE_BULLET {
b.DeletionTick = tick
}
}
}
func (s *PlayerStageState) CleanupOldBullets(tick int64) {
s.bullets = slices.DeleteFunc(s.bullets, func(b *ffi.Bullet) bool {
if b.DeletionTick > ACTIVE_BULLET && tick-b.DeletionTick > GRACE_WINDOW_TICKS {
ffi.DestroyBullet(b)
return true
}
return false
})
}
func (s *PlayerStageState) Kill(tick int64) {
s.deathTick = tick
s.dead = true
}
func (s *PlayerStageState) Revive() {
s.hitCol.UpdatePos(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1)
s.grazeCol.UpdatePos(STAGE_WIDTH*0.5, STAGE_HEIGHT-STAGE_HEIGHT*0.1)
s.updatePlayerPos = true
s.playerInputs = nil
s.dead = false
s.deathTick = PLAYER_ALIVE
}
func (s *PlayerStageState) CancelDeath() {
s.dead = false
s.deathTick = PLAYER_ALIVE
s.cancelDeath = true
}
func (s *PlayerStageState) HandleDeath(tick int64) {
// If the player didn't survive the grace window and they're currently alive, kill them
if !(s.survivedGraceWindow) && (s.dead == false) {
s.Kill(tick)
return
}
// If the player is currently dead and they survived the grace window, cancel their death
if (s.dead == true) && s.survivedGraceWindow {
s.CancelDeath()
return
}
// If the player is currently dead and they have been dead for greater than PLAYER_DEATH_TIMER_MAX ticks, revive them
if s.dead == true && ((tick - s.deathTick) > PLAYER_DEATH_TIMER_MAX) {
s.Revive()
return
}
}
func (s *PlayerStageState) CheckPlayerDeadOnTick(tick int64) bool {
if slices.ContainsFunc(s.bullets, func(b *ffi.Bullet) bool {
return b.CollidesWith(s.hitCol, tick)
}) {
return true
}
return false
}
func (s *PlayerStageState) CalculateGrazeDelta(tick int64) int {
count := 0
for _, b := range s.bullets {
if b.CollidesWith(s.grazeCol, tick) {
count++
}
}
return count * GRAZE_ADDITION_MULTIPLIER
return clamped
}
func (s *PlayerStageState) AddBullet(b *ffi.Bullet) {
@ -228,28 +225,33 @@ func MakeTestFireBullets(tick int64) []*ffi.Bullet {
return bullets
}
func (s *PlayerStageState) GetBoardStateDiff(tick int64) []map[string]any {
var newBulletsToBroadcast = []map[string]any{}
if !s.dead {
// Contains most timed server-side game logic
func (s *PlayerStageState) MakeServerEvents(tick int64) {
if s.deathState == ALIVE {
newBullets := MakeTestFireBullets(tick)
for _, bullet := range newBullets {
s.AddBullet(bullet)
newBulletsToBroadcast = append(newBulletsToBroadcast, bullet.Serialize(tick))
}
}
return newBulletsToBroadcast
}
func (s *PlayerStageState) MakeServerTick(tick int64) *ServerTickUpdate {
hitPosX, hitPosY := s.hitCol.GetPos()
grazePosX, grazePosY := s.grazeCol.GetPos()
prospectiveGraze := 0
for _, v := range s.grazePerTick {
prospectiveGraze += v
func (s *PlayerStageState) SerializeServerEventsWithinGraceWindow(tick int64) []map[string]any {
var bulletsWithinGraceWindow = []map[string]any{}
for _, bullet := range s.bullets {
if bullet.GetSpawnTick() > tick-GRACE_WINDOW_TICKS {
bulletsWithinGraceWindow = append(bulletsWithinGraceWindow, bullet.Serialize())
}
}
return bulletsWithinGraceWindow
}
func (s *PlayerStageState) SimulateAndMakeServerTick(tick int64) *ServerTickUpdate {
transientCurrentState := s.NewStateFromInputs(s.playerInputs, tick)
hitPosX, hitPosY := transientCurrentState.hitCol.GetPos()
grazePosX, grazePosY := transientCurrentState.grazeCol.GetPos()
var tickData = ServerTickUpdate{
Tick: tick,
PlayerHitPos: map[string]any{
@ -262,17 +264,11 @@ func (s *PlayerStageState) MakeServerTick(tick int64) *ServerTickUpdate {
"y": grazePosY,
"radius": STAGE_WIDTH * PLAYER_GRAZE_COL_RADIUS_MULTIPLIER,
},
StageStateDiff: s.GetBoardStateDiff(tick),
ForcePlayerPos: s.updatePlayerPos,
Graze: s.graze + prospectiveGraze,
Dead: s.dead,
CancelDeath: s.cancelDeath,
DeathTick: s.deathTick,
UTCTime: float64(time.Now().UnixMilli()) / 1000.0,
ServerEvents: s.SerializeServerEventsWithinGraceWindow(tick),
Graze: transientCurrentState.graze,
DeathState: transientCurrentState.deathState,
}
s.cancelDeath = false
s.updatePlayerPos = false
transientCurrentState.Delete()
return &tickData
}

View file

@ -23,8 +23,8 @@ type PresenceState struct { // present time! hahahahahahahah!
// Struct to serialize client->server updates
type ClientUpdate struct {
X float64 `json:"x"`
Y float64 `json:"y"`
VelX float64 `json:"vel_x"`
VelY float64 `json:"vel_y"`
Tick int64 `json:"tick"`
}
@ -33,11 +33,7 @@ type ServerTickUpdate struct {
Tick int64 `json:"tick"`
PlayerHitPos map[string]any `json:"playerHitPos"`
PlayerGrazePos map[string]any `json:"playerGrazePos"`
StageStateDiff []map[string]any `json:"stageStateDiff"`
ForcePlayerPos bool `json:"forcePlayerPos"`
Dead bool `json:"dead"`
CancelDeath bool `json:"cancelDeath"`
DeathTick int64 `json:"deathTick"`
ServerEvents []map[string]any `json:"serverEvents"`
DeathState int `json:"deathState"`
Graze int `json:"graze"`
UTCTime float64 `json:"utctime"`
}