diff --git a/action/action.go b/action/action.go index 9247641..e754146 100644 --- a/action/action.go +++ b/action/action.go @@ -1,6 +1,8 @@ package action import ( + "time" + "github.com/xescugc/maze-wars/utils" "github.com/xescugc/maze-wars/utils/graph" "nhooyr.io/websocket" @@ -27,6 +29,7 @@ type Action struct { StartGame *StartGamePayload `json:"start_game,omitempty"` GoHome *GoHomePayload `json:"go_home,omitempty"` ToggleStats *ToggleStatsPayload `json:"toggle_stats,omitempty"` + TPS *TPSPayload `json:"tps,omitempty"` OpenTowerMenu *OpenTowerMenuPayload `json:"open_tower_menu,omitempty"` CloseTowerMenu *CloseTowerMenuPayload `json:"close_tower_menu,omitempty"` @@ -79,9 +82,16 @@ func NewSummonUnit(t, pid string, plid, clid int) *Action { } } -func NewTPS() *Action { +type TPSPayload struct { + Time time.Time +} + +func NewTPS(t time.Time) *Action { return &Action{ Type: TPS, + TPS: &TPSPayload{ + Time: t, + }, } } @@ -509,8 +519,9 @@ type SyncStateUnitPayload struct { Health float64 - Path []graph.Step - HashPath string + Path []graph.Step + HashPath string + CreatedAt time.Time } // TODO: or make the action.Action separated or make the store.Player separated diff --git a/client/game/action.go b/client/game/action.go index 51fefb2..b28d97a 100644 --- a/client/game/action.go +++ b/client/game/action.go @@ -58,7 +58,7 @@ func (ac *ActionDispatcher) SummonUnit(unit, pid string, plid, clid int) { // TPS is the call for every TPS event func (ac *ActionDispatcher) TPS() { - tpsa := action.NewTPS() + tpsa := action.NewTPS(time.Time{}) ac.Dispatch(tpsa) } diff --git a/server/action.go b/server/action.go index 8dfef7b..c3d130f 100644 --- a/server/action.go +++ b/server/action.go @@ -81,11 +81,6 @@ func (ac *ActionDispatcher) WaitRoomCountdownTick() { ac.startGame() } -func (ac *ActionDispatcher) TPS(rooms *RoomsStore) { - tpsa := action.NewTPS() - ac.Dispatch(tpsa) -} - func (ac *ActionDispatcher) UserSignUp(un string) { ac.Dispatch(action.NewUserSignUp(un)) } @@ -103,6 +98,7 @@ func (ac *ActionDispatcher) UserSignOut(un string) { } func (ac *ActionDispatcher) SyncState(rooms *RoomsStore) { + ac.Dispatch(action.NewTPS(time.Now())) rstate := rooms.GetState().(RoomsState) for _, r := range rstate.Rooms { if r.Name == rstate.CurrentWaitingRoom { diff --git a/server/assets/wasm/maze-wars.wasm b/server/assets/wasm/maze-wars.wasm index ee85624..6864733 100755 Binary files a/server/assets/wasm/maze-wars.wasm and b/server/assets/wasm/maze-wars.wasm differ diff --git a/server/new.go b/server/new.go index 771db4a..4a2dcae 100644 --- a/server/new.go +++ b/server/new.go @@ -180,7 +180,6 @@ func startLoop(ctx context.Context, s *Store) { stateTicker := time.NewTicker(time.Second / 4) // The default TPS on of Ebiten client if 60 so to // emulate that we trigger the move action every TPS - tpsTicker := time.NewTicker(time.Second / 60) usersTicker := time.NewTicker(5 * time.Second) for { select { @@ -190,14 +189,11 @@ func startLoop(ctx context.Context, s *Store) { actionDispatcher.IncomeTick(s.Rooms) actionDispatcher.WaitRoomCountdownTick() actionDispatcher.SyncWaitingRoom(s.Rooms) - case <-tpsTicker.C: - actionDispatcher.TPS(s.Rooms) case <-usersTicker.C: actionDispatcher.SyncUsers(s.Users) case <-ctx.Done(): stateTicker.Stop() secondTicker.Stop() - tpsTicker.Stop() usersTicker.Stop() goto FINISH } diff --git a/store/lines.go b/store/lines.go index 2a98eca..4e64553 100644 --- a/store/lines.go +++ b/store/lines.go @@ -2,6 +2,7 @@ package store import ( "sync" + "time" "github.com/gofrs/uuid" "github.com/xescugc/go-flux" @@ -12,7 +13,13 @@ import ( "github.com/xescugc/maze-wars/utils/graph" ) -const atScale = true +const ( + atScale = true +) + +var ( + tpsMS = (time.Second / 60).Milliseconds() +) type Lines struct { *flux.ReduceStore @@ -31,6 +38,17 @@ type Line struct { Units map[string]*Unit Graph *graph.Graph + + // UpdatedAt is the last time + // something was updated on this Line. + // Towers added, Units added or + // when the Units position was updated + // the last time. + // Used for the SyncState to know how much + // time has passed since the last update + // and move the Units accordingly + // (60 moves per second pass) + UpdatedAt time.Time } type Tower struct { @@ -59,6 +77,12 @@ type Unit struct { Path []graph.Step HashPath string + + // CreatedAt has the time of creation so + // on the next SyncState will be moved just + // the diff amount and then it'll be set to 'nil' + // so we know it's on sync + CreatedAt time.Time } func (u *Unit) FacesetKey() string { return unit.Units[u.Type].FacesetKey() } @@ -168,19 +192,13 @@ func (ls *Lines) Reduce(state, a interface{}) interface{} { l.Towers[tw.ID] = tw - for _, u := range l.Units { - u.Path = l.Graph.AStar(u.X, u.Y, u.Facing, l.Graph.DeathNode.X, l.Graph.DeathNode.Y, atScale) - u.HashPath = graph.HashSteps(u.Path) - } + recalculateLineUnitSteps(l) case action.RemoveTower: // TODO: Add the LineID for _, l := range lstate.Lines { if ok := l.Graph.RemoveTower(act.RemoveTower.TowerID); ok { delete(l.Towers, act.RemoveTower.TowerID) - for _, u := range l.Units { - u.Path = l.Graph.AStar(u.X, u.Y, u.Facing, l.Graph.DeathNode.X, l.Graph.DeathNode.Y, atScale) - u.HashPath = graph.HashSteps(u.Path) - } + recalculateLineUnitSteps(l) } } case action.TowerAttack: @@ -225,6 +243,7 @@ func (ls *Lines) Reduce(state, a interface{}) interface{} { PlayerLineID: act.SummonUnit.PlayerLineID, CurrentLineID: act.SummonUnit.CurrentLineID, Health: unit.Units[act.SummonUnit.Type].Health, + CreatedAt: time.Now(), } u.Path = l.Graph.AStar(u.X, u.Y, u.Facing, l.Graph.DeathNode.X, l.Graph.DeathNode.Y, atScale) @@ -252,6 +271,7 @@ func (ls *Lines) Reduce(state, a interface{}) interface{} { u.Path = nl.Graph.AStar(u.X, u.Y, u.Facing, nl.Graph.DeathNode.X, nl.Graph.DeathNode.Y, atScale) u.HashPath = graph.HashSteps(u.Path) + u.CreatedAt = time.Now() nl.Units[u.ID] = u break @@ -273,16 +293,7 @@ func (ls *Lines) Reduce(state, a interface{}) interface{} { defer ls.mxLines.Unlock() for _, l := range lstate.Lines { - for _, u := range l.Units { - if len(u.Path) > 0 { - nextStep := u.Path[0] - u.Path = u.Path[1:] - u.MovingCount += 1 - u.Y = nextStep.Y - u.X = nextStep.X - u.Facing = nextStep.Facing - } - } + moveLineUnitsTo(l, act.TPS.Time) } case action.RemovePlayer: ls.mxLines.Lock() @@ -365,6 +376,48 @@ func (ls *Lines) Reduce(state, a interface{}) interface{} { return lstate } +func recalculateLineUnitSteps(l *Line) { + t := time.Now() + moveLineUnitsTo(l, t) + + for _, u := range l.Units { + u.Path = l.Graph.AStar(u.X, u.Y, u.Facing, l.Graph.DeathNode.X, l.Graph.DeathNode.Y, atScale) + u.HashPath = graph.HashSteps(u.Path) + } +} + +func moveLineUnitsTo(l *Line, t time.Time) { + lmoves := 1 + if !t.IsZero() && !l.UpdatedAt.IsZero() { + lmoves = int(t.Sub(l.UpdatedAt).Milliseconds() / tpsMS) + } + for _, u := range l.Units { + if len(u.Path) > 0 { + umoves := lmoves + if !t.IsZero() && !u.CreatedAt.IsZero() { + umoves = int(t.Sub(u.CreatedAt).Milliseconds() / tpsMS) + // This way we mean it's up to date now + u.CreatedAt = time.Time{} + } + // If we have less moves remaining that the expected amount + // we just move to the last position + if len(u.Path) < umoves { + umoves = len(u.Path) - 1 + } + if umoves == 0 { + continue + } + nextStep := u.Path[umoves-1] + u.Path = u.Path[umoves:] + u.MovingCount += umoves + u.Y = nextStep.Y + u.X = nextStep.X + u.Facing = nextStep.Facing + } + } + l.UpdatedAt = t +} + func (ls *Lines) newLine(lid int) *Line { x, y := ls.store.Map.GetHomeCoordinates(lid) g, err := graph.New(x+16, y+16, 16, 84, 16, 7, 74, 3) diff --git a/store/reduce_test.go b/store/reduce_test.go index 09be5b1..23b0b6a 100644 --- a/store/reduce_test.go +++ b/store/reduce_test.go @@ -6,6 +6,7 @@ import ( "sort" "sync" "testing" + "time" "github.com/gofrs/uuid" "github.com/stretchr/testify/assert" @@ -237,24 +238,57 @@ func TestSummonUnit(t *testing.T) { func TestTPS(t *testing.T) { addAction(action.TPS.String()) t.Run("Success", func(t *testing.T) { - s := initStore() - p := addPlayer(s) - p2 := addPlayer(s) - s.Dispatch(action.NewStartGame()) - ms, ls := startGame(t, s) - p, u := summonUnit(s, p, p2) - - s.Dispatch(action.NewTPS()) - - ps := playersInitialState() - ps.Players[p.ID] = &p - ps.Players[p2.ID] = &p2 - - u.Path = u.Path[1:] - u.MovingCount++ - ls.Lines[p2.LineID].Units[u.ID] = &u - - equalStore(t, s, ps, ms, ls) + t.Run("Default", func(t *testing.T) { + s := initStore() + p := addPlayer(s) + p2 := addPlayer(s) + s.Dispatch(action.NewStartGame()) + ms, ls := startGame(t, s) + p, u := summonUnit(s, p, p2) + + s.Dispatch(action.NewTPS(time.Time{})) + + ps := playersInitialState() + ps.Players[p.ID] = &p + ps.Players[p2.ID] = &p2 + + u.Path = u.Path[1:] + u.MovingCount++ + ls.Lines[p2.LineID].Units[u.ID] = &u + + equalStore(t, s, ps, ms, ls) + }) + t.Run("WithTime", func(t *testing.T) { + s := initStore() + p := addPlayer(s) + p2 := addPlayer(s) + s.Dispatch(action.NewStartGame()) + ms, ls := startGame(t, s) + p, u := summonUnit(s, p, p2) + + l2 := s.Lines.GetState().(store.LinesState).Lines[p2.LineID] + + tn := time.Now() + l2.UpdatedAt = tn + l2.Units[u.ID].CreatedAt = tn + + ta := tn.Add(time.Second) + s.Dispatch(action.NewTPS(ta)) + + ps := playersInitialState() + ps.Players[p.ID] = &p + ps.Players[p2.ID] = &p2 + + np := u.Path[61] + u.Path = u.Path[62:] + u.MovingCount += 62 + u.X = np.X + u.Y = np.Y + u.Facing = np.Facing + ls.Lines[p2.LineID].Units[u.ID] = &u + + equalStore(t, s, ps, ms, ls) + }) }) } @@ -489,6 +523,8 @@ func TestTowerAttack(t *testing.T) { p := addPlayer(s) p2 := addPlayer(s) s.Dispatch(action.NewStartGame()) + // TODO: Each summon/place updates the l.UpdatedAt so we should + // manually add the value from the store line ms, ls := startGame(t, s) p, tw := placeTower(s, p) p, u := summonUnit(s, p, p2) @@ -634,6 +670,7 @@ func TestChangeUnitLine(t *testing.T) { // As this are random assigned we cannot expect them u1.ID, u1.X, u1.Y = units[uid].ID, units[uid].X, units[uid].Y + u1.CreatedAt = units[uid].CreatedAt // We need to set the path after the X, Y are set u1.Path = l.Graph.AStar(u1.X, u1.Y, u1.Facing, l.Graph.DeathNode.X, l.Graph.DeathNode.Y, atScale) @@ -763,9 +800,17 @@ func equalStore(t *testing.T, sto *store.Store, states ...interface{}) { // to have the Units/Towers init for _, l := range lis.Lines { l.Graph = nil + l.UpdatedAt = time.Time{} + for _, u := range l.Units { + u.CreatedAt = time.Time{} + } } for _, l := range sto.Lines.GetState().(store.LinesState).Lines { l.Graph = nil + l.UpdatedAt = time.Time{} + for _, u := range l.Units { + u.CreatedAt = time.Time{} + } } assert.Equal(t, lis, sto.Lines.GetState().(store.LinesState)) assert.Equal(t, mis, sto.Map.GetState().(store.MapState))