diff --git a/action/action.go b/action/action.go index fc4b55a..3e29578 100644 --- a/action/action.go +++ b/action/action.go @@ -22,11 +22,11 @@ type Action struct { TowerAttack *TowerAttackPayload `json:"tower_attack,omitempty"` UnitKilled *UnitKilledPayload `json:"unit_killed,omitempty"` WindowResizing *WindowResizingPayload `json:"window_resizing,omitempty"` - PlayerReady *PlayerReadyPayload `json:"player_ready, omitempty"` NavigateTo *NavigateToPayload `json:"navigate_to, omitempty"` StartGame *StartGamePayload `json:"start_game, omitempty"` GoHome *GoHomePayload `json:"go_home, omitempty"` CheckedPath *CheckedPathPayload `json:"checked_path,omitempty"` + ToggleStats *ToggleStatsPayload `json:"toggle_stats,omitempty"` OpenTowerMenu *OpenTowerMenuPayload `json:"open_tower_menu, omitempty"` CloseTowerMenu *CloseTowerMenuPayload `json:"close_tower_menu, omitempty"` @@ -284,28 +284,13 @@ func NewAddPlayer(id, name string, lid int) *Action { } type RemovePlayerPayload struct { - ID string - Room string + ID string } -func NewRemovePlayer(r, id string) *Action { +func NewRemovePlayer(id string) *Action { return &Action{ Type: RemovePlayer, RemovePlayer: &RemovePlayerPayload{ - ID: id, - Room: r, - }, - } -} - -type PlayerReadyPayload struct { - ID string -} - -func NewPlayerReady(id string) *Action { - return &Action{ - Type: PlayerReady, - PlayerReady: &PlayerReadyPayload{ ID: id, }, } @@ -325,15 +310,12 @@ func NewNavigateTo(route string) *Action { } type StartGamePayload struct { - Room string } -func NewStartGame(r string) *Action { +func NewStartGame() *Action { return &Action{ - Type: StartGame, - StartGame: &StartGamePayload{ - Room: r, - }, + Type: StartGame, + StartGame: &StartGamePayload{}, } } @@ -381,6 +363,16 @@ func NewCheckedPath(cp bool) *Action { } } +type ToggleStatsPayload struct { +} + +func NewToggleStats() *Action { + return &Action{ + Type: ToggleStats, + ToggleStats: &ToggleStatsPayload{}, + } +} + type SignUpErrorPayload struct { Error string } @@ -500,7 +492,6 @@ type UpdateStatePlayerPayload struct { Gold int Current bool Winner bool - Ready bool } type UpdateStateTowersPayload struct { diff --git a/action/type.go b/action/type.go index a7521b7..f290f05 100644 --- a/action/type.go +++ b/action/type.go @@ -35,6 +35,7 @@ const ( UserSignOut JoinWaitingRoom ExitWaitingRoom + ToggleStats // Specific to WS AddPlayer diff --git a/action/type_string.go b/action/type_string.go index b1bf9c1..bda05a8 100644 --- a/action/type_string.go +++ b/action/type_string.go @@ -8,11 +8,11 @@ import ( "strings" ) -const _TypeName = "cursor_movecamera_zoomsummon_unittpsremove_unitsteal_liveplace_towerremove_towerselect_towerselected_towerselected_tower_invaliddeselect_towerincome_ticktower_attackunit_killedwindow_resizingplayer_readynavigate_tostart_gameopen_tower_menuclose_tower_menugo_homechecked_pathchange_unit_linesign_up_erroruser_sign_upuser_sign_inuser_sign_outjoin_waiting_roomexit_waiting_roomadd_playerremove_playerupdate_stateupdate_userswait_room_countdown_ticksync_waiting_room" +const _TypeName = "cursor_movecamera_zoomsummon_unittpsremove_unitsteal_liveplace_towerremove_towerselect_towerselected_towerselected_tower_invaliddeselect_towerincome_ticktower_attackunit_killedwindow_resizingplayer_readynavigate_tostart_gameopen_tower_menuclose_tower_menugo_homechecked_pathchange_unit_linesign_up_erroruser_sign_upuser_sign_inuser_sign_outjoin_waiting_roomexit_waiting_roomtoggle_statsadd_playerremove_playerupdate_stateupdate_userswait_room_countdown_ticksync_waiting_room" -var _TypeIndex = [...]uint16{0, 11, 22, 33, 36, 47, 57, 68, 80, 92, 106, 128, 142, 153, 165, 176, 191, 203, 214, 224, 239, 255, 262, 274, 290, 303, 315, 327, 340, 357, 374, 384, 397, 409, 421, 445, 462} +var _TypeIndex = [...]uint16{0, 11, 22, 33, 36, 47, 57, 68, 80, 92, 106, 128, 142, 153, 165, 176, 191, 203, 214, 224, 239, 255, 262, 274, 290, 303, 315, 327, 340, 357, 374, 386, 396, 409, 421, 433, 457, 474} -const _TypeLowerName = "cursor_movecamera_zoomsummon_unittpsremove_unitsteal_liveplace_towerremove_towerselect_towerselected_towerselected_tower_invaliddeselect_towerincome_ticktower_attackunit_killedwindow_resizingplayer_readynavigate_tostart_gameopen_tower_menuclose_tower_menugo_homechecked_pathchange_unit_linesign_up_erroruser_sign_upuser_sign_inuser_sign_outjoin_waiting_roomexit_waiting_roomadd_playerremove_playerupdate_stateupdate_userswait_room_countdown_ticksync_waiting_room" +const _TypeLowerName = "cursor_movecamera_zoomsummon_unittpsremove_unitsteal_liveplace_towerremove_towerselect_towerselected_towerselected_tower_invaliddeselect_towerincome_ticktower_attackunit_killedwindow_resizingplayer_readynavigate_tostart_gameopen_tower_menuclose_tower_menugo_homechecked_pathchange_unit_linesign_up_erroruser_sign_upuser_sign_inuser_sign_outjoin_waiting_roomexit_waiting_roomtoggle_statsadd_playerremove_playerupdate_stateupdate_userswait_room_countdown_ticksync_waiting_room" func (i Type) String() string { if i < 0 || i >= Type(len(_TypeIndex)-1) { @@ -55,15 +55,16 @@ func _TypeNoOp() { _ = x[UserSignOut-(27)] _ = x[JoinWaitingRoom-(28)] _ = x[ExitWaitingRoom-(29)] - _ = x[AddPlayer-(30)] - _ = x[RemovePlayer-(31)] - _ = x[UpdateState-(32)] - _ = x[UpdateUsers-(33)] - _ = x[WaitRoomCountdownTick-(34)] - _ = x[SyncWaitingRoom-(35)] + _ = x[ToggleStats-(30)] + _ = x[AddPlayer-(31)] + _ = x[RemovePlayer-(32)] + _ = x[UpdateState-(33)] + _ = x[UpdateUsers-(34)] + _ = x[WaitRoomCountdownTick-(35)] + _ = x[SyncWaitingRoom-(36)] } -var _TypeValues = []Type{CursorMove, CameraZoom, SummonUnit, TPS, RemoveUnit, StealLive, PlaceTower, RemoveTower, SelectTower, SelectedTower, SelectedTowerInvalid, DeselectTower, IncomeTick, TowerAttack, UnitKilled, WindowResizing, PlayerReady, NavigateTo, StartGame, OpenTowerMenu, CloseTowerMenu, GoHome, CheckedPath, ChangeUnitLine, SignUpError, UserSignUp, UserSignIn, UserSignOut, JoinWaitingRoom, ExitWaitingRoom, AddPlayer, RemovePlayer, UpdateState, UpdateUsers, WaitRoomCountdownTick, SyncWaitingRoom} +var _TypeValues = []Type{CursorMove, CameraZoom, SummonUnit, TPS, RemoveUnit, StealLive, PlaceTower, RemoveTower, SelectTower, SelectedTower, SelectedTowerInvalid, DeselectTower, IncomeTick, TowerAttack, UnitKilled, WindowResizing, PlayerReady, NavigateTo, StartGame, OpenTowerMenu, CloseTowerMenu, GoHome, CheckedPath, ChangeUnitLine, SignUpError, UserSignUp, UserSignIn, UserSignOut, JoinWaitingRoom, ExitWaitingRoom, ToggleStats, AddPlayer, RemovePlayer, UpdateState, UpdateUsers, WaitRoomCountdownTick, SyncWaitingRoom} var _TypeNameToValueMap = map[string]Type{ _TypeName[0:11]: CursorMove, @@ -126,18 +127,20 @@ var _TypeNameToValueMap = map[string]Type{ _TypeLowerName[340:357]: JoinWaitingRoom, _TypeName[357:374]: ExitWaitingRoom, _TypeLowerName[357:374]: ExitWaitingRoom, - _TypeName[374:384]: AddPlayer, - _TypeLowerName[374:384]: AddPlayer, - _TypeName[384:397]: RemovePlayer, - _TypeLowerName[384:397]: RemovePlayer, - _TypeName[397:409]: UpdateState, - _TypeLowerName[397:409]: UpdateState, - _TypeName[409:421]: UpdateUsers, - _TypeLowerName[409:421]: UpdateUsers, - _TypeName[421:445]: WaitRoomCountdownTick, - _TypeLowerName[421:445]: WaitRoomCountdownTick, - _TypeName[445:462]: SyncWaitingRoom, - _TypeLowerName[445:462]: SyncWaitingRoom, + _TypeName[374:386]: ToggleStats, + _TypeLowerName[374:386]: ToggleStats, + _TypeName[386:396]: AddPlayer, + _TypeLowerName[386:396]: AddPlayer, + _TypeName[396:409]: RemovePlayer, + _TypeLowerName[396:409]: RemovePlayer, + _TypeName[409:421]: UpdateState, + _TypeLowerName[409:421]: UpdateState, + _TypeName[421:433]: UpdateUsers, + _TypeLowerName[421:433]: UpdateUsers, + _TypeName[433:457]: WaitRoomCountdownTick, + _TypeLowerName[433:457]: WaitRoomCountdownTick, + _TypeName[457:474]: SyncWaitingRoom, + _TypeLowerName[457:474]: SyncWaitingRoom, } var _TypeNames = []string{ @@ -171,12 +174,13 @@ var _TypeNames = []string{ _TypeName[327:340], _TypeName[340:357], _TypeName[357:374], - _TypeName[374:384], - _TypeName[384:397], - _TypeName[397:409], + _TypeName[374:386], + _TypeName[386:396], + _TypeName[396:409], _TypeName[409:421], - _TypeName[421:445], - _TypeName[445:462], + _TypeName[421:433], + _TypeName[433:457], + _TypeName[457:474], } // TypeString retrieves an enum value from the enum constants string name. diff --git a/assets/TilesetElement.png b/assets/TilesetElement.png deleted file mode 100644 index 372b134..0000000 Binary files a/assets/TilesetElement.png and /dev/null differ diff --git a/assets/assets.go b/assets/assets.go index aee5509..5f7a0b8 100644 --- a/assets/assets.go +++ b/assets/assets.go @@ -75,9 +75,6 @@ var TilesetHouse_png []byte //go:embed TilesetLogic.png var TilesetLogic_png []byte -//go:embed TilesetElement.png -var TilesetElement_png []byte - //go:embed maps/2.png var Map_2_png []byte diff --git a/client/action.go b/client/action.go index 1ecc40e..4d164f4 100644 --- a/client/action.go +++ b/client/action.go @@ -130,11 +130,11 @@ func (ac *ActionDispatcher) UnitKilled(pid, ut string) { ac.dispatcher.Dispatch(uk) } -// PlayerReady marks the player as ready to start the game -func (ac *ActionDispatcher) PlayerReady(pid string) { - pr := action.NewPlayerReady(pid) - wsSend(pr) - ac.dispatcher.Dispatch(pr) +func (ac *ActionDispatcher) RemovePlayer(pid string) { + rpa := action.NewRemovePlayer(pid) + wsSend(rpa) + ac.dispatcher.Dispatch(rpa) + ac.dispatcher.Dispatch(action.NewNavigateTo(LobbyRoute)) } // WindowResizing new sizes of the window @@ -149,14 +149,6 @@ func (ac *ActionDispatcher) NavigateTo(route string) { ac.dispatcher.Dispatch(nt) } -// StartGame notifies that the game will start, -// used to update any store before that -func (ac *ActionDispatcher) StartGame(r string) { - sg := action.NewStartGame(r) - wsSend(sg) - ac.dispatcher.Dispatch(sg) -} - // OpenTowerMenu when a tower is clicked and the menu of // the tower is displayed func (ac *ActionDispatcher) OpenTowerMenu(tid string) { @@ -256,3 +248,9 @@ func (ac *ActionDispatcher) ExitWaitingRoom(un string) { ac.dispatcher.Dispatch(action.NewNavigateTo(LobbyRoute)) } + +func (ac *ActionDispatcher) ToggleStats() { + tsa := action.NewToggleStats() + + ac.dispatcher.Dispatch(tsa) +} diff --git a/client/game.go b/client/game.go index ceec268..26616ce 100644 --- a/client/game.go +++ b/client/game.go @@ -1,9 +1,8 @@ package client import ( - "image" - "github.com/hajimehoshi/ebiten/v2" + "github.com/xescugc/maze-wars/action" "github.com/xescugc/maze-wars/store" ) @@ -19,35 +18,31 @@ type Game struct { Units *Units Towers *Towers - SessionID string + Map *Map } func (g *Game) Update() error { + g.Map.Update() g.Camera.Update() g.HUD.Update() g.Units.Update() g.Towers.Update() + if len(g.Store.Players.List()) == 0 { + actionDispatcher.Dispatch(action.NewAddPlayer("1", "test1", 0)) + actionDispatcher.Dispatch(action.NewAddPlayer("2", "test2", 1)) + actionDispatcher.Dispatch(action.NewAddPlayer("3", "test3", 2)) + actionDispatcher.Dispatch(action.NewAddPlayer("4", "test4", 3)) + actionDispatcher.Dispatch(action.NewAddPlayer("5", "test5", 4)) + actionDispatcher.Dispatch(action.NewAddPlayer("6", "test6", 5)) + } actionDispatcher.TPS() return nil } func (g *Game) Draw(screen *ebiten.Image) { - - // Draw will draw just a partial image of the map based on the viewport, so it does not render everything but just the - // part that it's seen by the user - // If we want to render everything and just move the viewport around we need o render the full image and change the - // opt.GeoM.Transport to the Map.X/Y and change the Update function to do the opposite in terms of -+ - // - // TODO: Maybe create a self Map entity with Update/Draw - op := &ebiten.DrawImageOptions{} - s := g.Camera.GetState().(CameraState) - op.GeoM.Scale(s.Zoom, s.Zoom) - inverseZoom := maxZoom - s.Zoom + zoomScale - mi := ebiten.NewImageFromImage(g.Store.Map.GetState().(store.MapState).Image) - screen.DrawImage(mi.SubImage(image.Rect(int(s.X), int(s.Y), int((s.X+s.W)*inverseZoom), int((s.Y+s.H)*inverseZoom))).(*ebiten.Image), op) - + g.Map.Draw(screen) g.Camera.Draw(screen) g.HUD.Draw(screen) g.Units.Draw(screen) diff --git a/client/hud.go b/client/hud.go index 0bd23a4..10ad158 100644 --- a/client/hud.go +++ b/client/hud.go @@ -1,20 +1,19 @@ package client import ( - "bytes" "fmt" - "image" "image/color" "math" "sort" "strconv" "strings" + "github.com/ebitenui/ebitenui" + "github.com/ebitenui/ebitenui/image" + "github.com/ebitenui/ebitenui/widget" "github.com/hajimehoshi/ebiten/v2" - "github.com/hajimehoshi/ebiten/v2/text" "github.com/xescugc/go-flux" "github.com/xescugc/maze-wars/action" - "github.com/xescugc/maze-wars/assets" "github.com/xescugc/maze-wars/inputer" "github.com/xescugc/maze-wars/store" "github.com/xescugc/maze-wars/tower" @@ -29,33 +28,26 @@ type HUDStore struct { game *Game - houseIcon image.Image + ui *ebitenui.UI input inputer.Inputer + + statsListW *widget.List + incomeTextW *widget.Text + winLoseTextW *widget.Text + unitsC *widget.Container + towersC *widget.Container } // HUDState stores the HUD state type HUDState struct { - Units []unitFacesetButton - Towers []towerFacesetButton - - HouseButton utils.Object - SelectedTower *SelectedTower TowerOpenMenuID string LastCursorPosition utils.Object CheckedPath bool -} - -type unitFacesetButton struct { - Unit *unit.Unit - Object utils.Object -} -type towerFacesetButton struct { - Tower *tower.Tower - Object utils.Object + ShowStats bool } type SelectedTower struct { @@ -66,45 +58,23 @@ type SelectedTower struct { // NewHUDStore creates a new HUDStore with the Dispatcher d and the Game g func NewHUDStore(d *flux.Dispatcher, i inputer.Inputer, g *Game) (*HUDStore, error) { - us := make([]*unit.Unit, 0, 0) - for _, u := range unit.Units { - us = append(us, u) - } - sort.Slice(us, func(i, j int) bool { - return us[i].Gold < us[j].Gold - }) - - cs := g.Camera.GetState().(CameraState) - - ubs := calculateHUDUnitButtons(cs) - tbs := calculateHUDTowerButtons(cs) - hi, _, err := image.Decode(bytes.NewReader(assets.TilesetElement_png)) - if err != nil { - return nil, err - } - hs := &HUDStore{ game: g, - houseIcon: ebiten.NewImageFromImage(hi).SubImage(image.Rect(12*16, 0*16, 12*16+16, 0*16+16)), - input: i, } hs.ReduceStore = flux.NewReduceStore(d, hs.Reduce, HUDState{ - Units: ubs, - Towers: tbs, - HouseButton: utils.Object{ - X: float64(cs.W - 16), - Y: 0, - W: float64(16), - H: float64(16), - }, + ShowStats: true, }) + hs.buildUI() + return hs, nil } func (hs *HUDStore) Update() error { + hs.ui.Update() + cs := hs.game.Camera.GetState().(CameraState) hst := hs.GetState().(HUDState) x, y := hs.input.CursorPosition() @@ -121,33 +91,11 @@ func (hs *HUDStore) Update() error { return nil } if hs.input.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { - click := utils.Object{ - X: float64(x), - Y: float64(y), - W: 1, H: 1, - } clickAbsolute := utils.Object{ X: float64(x) + cs.X, Y: float64(y) + cs.Y, W: 1, H: 1, } - // Check what the user has just clicked - for _, u := range hst.Units { - if cp.CanSummonUnit(u.Unit.Type.String()) && u.Object.IsColliding(click) { - actionDispatcher.SummonUnit(u.Unit.Type.String(), cp.ID, cp.LineID, hs.game.Store.Map.GetNextLineID(cp.LineID)) - return nil - } - } - for _, t := range hst.Towers { - if cp.CanPlaceTower(t.Tower.Type.String()) && t.Object.IsColliding(click) { - actionDispatcher.SelectTower(t.Tower.Type.String(), x, y) - return nil - } - } - if hst.HouseButton.IsColliding(click) { - actionDispatcher.GoHome() - return nil - } if hst.SelectedTower != nil && !hst.SelectedTower.Invalid { // We double check that placing the tower would not block the path @@ -276,45 +224,73 @@ func (hs *HUDStore) Update() error { } func (hs *HUDStore) Draw(screen *ebiten.Image) { + hst := hs.GetState().(HUDState) cs := hs.game.Camera.GetState().(CameraState) cp := hs.game.Store.Players.FindCurrent() - if cp.Lives == 0 { - text.Draw(screen, "YOU LOST", smallFont, int(cs.W/2), int(cs.H/2), color.White) + psit := hs.game.Store.Players.GetState().(store.PlayersState).IncomeTimer + entries := make([]any, 0, 0) + entries = append(entries, + fmt.Sprintf("%s %s %s", + fillIn("Name", 10), + fillIn("Lives", 8), + fillIn("Income", 8)), + ) + + var sortedPlayers = make([]*store.Player, 0, 0) + for _, p := range hs.game.Store.Players.List() { + sortedPlayers = append(sortedPlayers, p) + } + sort.Slice(sortedPlayers, func(i, j int) bool { + ii := sortedPlayers[i] + jj := sortedPlayers[j] + if ii.Income != jj.Income { + return ii.Income > jj.Income + } + return ii.LineID < jj.LineID + }) + for _, p := range sortedPlayers { + entries = append(entries, + fmt.Sprintf("%s %s %s", + fillIn(p.Name, 10), + fillIn(strconv.Itoa(p.Lives), 8), + fillIn(strconv.Itoa(p.Income), 8)), + ) } + hs.statsListW.SetEntries(entries) - if cp.Winner { - text.Draw(screen, "YOU WON!", smallFont, int(cs.W/2), int(cs.H/2), color.White) + visibility := widget.Visibility_Show + if !hst.ShowStats { + visibility = widget.Visibility_Hide_Blocking } + hs.statsListW.GetWidget().Visibility = visibility + hs.incomeTextW.Label = fmt.Sprintf("Gold: %s Income Timer: %ds", fillIn(strconv.Itoa(cp.Gold), 5), psit) - for _, u := range hst.Units { - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(u.Object.X, u.Object.Y) - if !cp.CanSummonUnit(u.Unit.Type.String()) { - op.ColorM.Scale(2, 0.5, 0.5, 0.9) - } - screen.DrawImage(ebiten.NewImageFromImage(u.Unit.Faceset), op) + wuts := hs.unitsC.Children() + for i, u := range sortedUnits() { + wuts[i].GetWidget().Disabled = !cp.CanSummonUnit(u.Type.String()) } - for _, t := range hst.Towers { - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(t.Object.X, t.Object.Y) - if !cp.CanPlaceTower(t.Tower.Type.String()) { - op.ColorM.Scale(2, 0.5, 0.5, 0.9) - } else if hst.SelectedTower != nil && hst.SelectedTower.Type == t.Tower.Type.String() { - // Once the tower is selected we gray it out - op.ColorM.Scale(0.5, 0.5, 0.5, 0.5) - } - screen.DrawImage(ebiten.NewImageFromImage(t.Tower.Faceset), op) + wtws := hs.towersC.Children() + for i, t := range sortedTowers() { + wtws[i].GetWidget().Disabled = !cp.CanPlaceTower(t.Type.String()) } - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(hst.HouseButton.X, hst.HouseButton.Y) - screen.DrawImage(hs.houseIcon.(*ebiten.Image), op) + if cp.Lives == 0 { + hs.winLoseTextW.Label = "YOU LOST" + hs.winLoseTextW.GetWidget().Visibility = widget.Visibility_Show + } + + if cp.Winner { + hs.winLoseTextW.Label = "YOU WON!" + hs.winLoseTextW.GetWidget().Visibility = widget.Visibility_Show + } + + hs.ui.Draw(screen) if hst.SelectedTower != nil { - op = &ebiten.DrawImageOptions{} + op := &ebiten.DrawImageOptions{} op.GeoM.Translate(hst.SelectedTower.X/cs.Zoom, hst.SelectedTower.Y/cs.Zoom) op.GeoM.Scale(cs.Zoom, cs.Zoom) @@ -324,46 +300,6 @@ func (hs *HUDStore) Draw(screen *ebiten.Image) { screen.DrawImage(ebiten.NewImageFromImage(hst.SelectedTower.Faceset()), op) } - - // To make the table for the players more readable we are gonna make a table, - // the table will have headers and here I'm gonna put the characters each one - // will have from left to right: - // * -Space-: 2 "|\s" - // * Name: 20 - // * -Space-: 3 "\s|\s" - // * Lives: 8 - // * -Space-: 3 "\s|\s" - // * Gold: 8 - // * -Space-: 3 "\s|\s" - // * Income: 8 - // * -Space-: 2 "\s|" - // Total of 57 - psit := hs.game.Store.Players.GetState().(store.PlayersState).IncomeTimer - players := hs.game.Store.Players.List() - text.Draw(screen, fmt.Sprintf("Income Timer: %ds", psit), smallFont, 0, 16, color.White) - var pcount = 4 - var sortedPlayers = make([]*store.Player, 0, 0) - for _, p := range players { - sortedPlayers = append(sortedPlayers, p) - } - sort.Slice(sortedPlayers, func(i, j int) bool { return sortedPlayers[i].LineID < sortedPlayers[j].LineID }) - text.Draw(screen, "---------------------------------------------------------", smallFont, 0, 32, color.White) - text.Draw(screen, "| Name | Lives | Gold | Income |", smallFont, 0, 48, color.White) - for _, p := range sortedPlayers { - var c color.Color = color.White - if p.ID == cp.ID { - c = green - } - text.Draw(screen, fmt.Sprintf( - "| %s | %s | %s | %s |", - fillIn(p.Name, 20), - fillIn(strconv.Itoa(p.Lives), 8), - fillIn(strconv.Itoa(p.Gold), 8), - fillIn(strconv.Itoa(p.Income), 8), - ), smallFont, 0, 16*pcount, c) - pcount++ - } - text.Draw(screen, "_________________________________________________________", smallFont, 0, 16*pcount, color.White) } func fillIn(s string, l int) string { @@ -391,19 +327,6 @@ func (hs *HUDStore) Reduce(state, a interface{}) interface{} { } switch act.Type { - case action.WindowResizing: - hs.GetDispatcher().WaitFor(hs.game.Camera.GetDispatcherToken()) - cs := hs.game.Camera.GetState().(CameraState) - - hstate.Units = calculateHUDUnitButtons(cs) - hstate.Towers = calculateHUDTowerButtons(cs) - - hstate.HouseButton = utils.Object{ - X: float64(cs.W - 16), - Y: 0, - W: 16, - H: 16, - } case action.SelectTower: hs.GetDispatcher().WaitFor(hs.game.Store.Players.GetDispatcherToken()) cp := hs.game.Store.Players.FindCurrent() @@ -457,13 +380,15 @@ func (hs *HUDStore) Reduce(state, a interface{}) interface{} { hstate.TowerOpenMenuID = "" case action.CheckedPath: hstate.CheckedPath = act.CheckedPath.Checked + case action.ToggleStats: + hstate.ShowStats = !hstate.ShowStats default: } return hstate } -func calculateHUDUnitButtons(cs CameraState) []unitFacesetButton { +func sortedUnits() []*unit.Unit { us := make([]*unit.Unit, 0, 0) for _, u := range unit.Units { us = append(us, u) @@ -471,29 +396,10 @@ func calculateHUDUnitButtons(cs CameraState) []unitFacesetButton { sort.Slice(us, func(i, j int) bool { return us[i].Gold < us[j].Gold }) - - // We want to create rows of 5 - fs := make([]unitFacesetButton, 0, 0) - nrows := len(us) / 5 - - // As all the Faceset are equal squares - // we just need to take one - fhw := float64(us[0].Faceset.Bounds().Dx()) - for i, u := range us { - fs = append(fs, unitFacesetButton{ - Unit: u, - Object: utils.Object{ - X: cs.W - (fhw * float64(5-(i%5))), - Y: cs.H - (fhw * float64(nrows-(i/5))), - W: fhw, - H: fhw, - }, - }) - } - return fs + return us } -func calculateHUDTowerButtons(cs CameraState) []towerFacesetButton { +func sortedTowers() []*tower.Tower { ts := make([]*tower.Tower, 0, 0) for _, t := range tower.Towers { ts = append(ts, t) @@ -501,26 +407,7 @@ func calculateHUDTowerButtons(cs CameraState) []towerFacesetButton { sort.Slice(ts, func(i, j int) bool { return ts[i].Type > ts[j].Type }) - - // We want to create rows of 5 - fs := make([]towerFacesetButton, 0, 0) - nrows := (len(ts) / 5) + 1 - - // As all the Faceset are equal squares - // we just need to take one - fhw := float64(ts[0].Faceset.Bounds().Dx()) - for i, t := range ts { - fs = append(fs, towerFacesetButton{ - Tower: t, - Object: utils.Object{ - X: 0 + (fhw * float64(i%5)), - Y: cs.H - (fhw * float64(nrows-(i/5))), - W: fhw, - H: fhw, - }, - }) - } - return fs + return ts } // closestMultiple finds the coses multiple of 'b' for the number 'a' @@ -529,3 +416,352 @@ func closestMultiple(a, b int) int { a = a - (a % b) return a } + +func (hs *HUDStore) buildUI() { + topRightContainer := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + ) + + topRightVerticalRowC := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewRowLayout( + widget.RowLayoutOpts.Direction(widget.DirectionVertical), + widget.RowLayoutOpts.Spacing(20), + )), + widget.ContainerOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{ + HorizontalPosition: widget.AnchorLayoutPositionEnd, + VerticalPosition: widget.AnchorLayoutPositionStart, + }), + ), + ) + + topRightVerticalRowWraperC := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewRowLayout( + widget.RowLayoutOpts.Direction(widget.DirectionVertical), + )), + widget.ContainerOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.RowLayoutData{ + Stretch: true, + }), + ), + ) + + topRightHorizontalRowC := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewRowLayout( + widget.RowLayoutOpts.Direction(widget.DirectionHorizontal), + widget.RowLayoutOpts.Spacing(20), + )), + widget.ContainerOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.RowLayoutData{ + Position: widget.RowLayoutPositionEnd, + }), + ), + ) + + homeBtnW := widget.NewButton( + widget.ButtonOpts.Image(buttonImage), + + widget.ButtonOpts.Text("HOME", smallFont, &widget.ButtonTextColor{ + Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff}, + }), + + // specify that the button's text needs some padding for correct display + widget.ButtonOpts.TextPadding(widget.Insets{ + Left: 30, + Right: 30, + Top: 5, + Bottom: 5, + }), + + widget.ButtonOpts.ClickedHandler(func(args *widget.ButtonClickedEventArgs) { + actionDispatcher.GoHome() + }), + ) + + statsBtnW := widget.NewButton( + widget.ButtonOpts.Image(buttonImage), + + widget.ButtonOpts.Text("STATS", smallFont, &widget.ButtonTextColor{ + Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff}, + }), + + widget.ButtonOpts.TextPadding(widget.Insets{ + Left: 30, + Right: 30, + Top: 5, + Bottom: 5, + }), + + widget.ButtonOpts.ClickedHandler(func(args *widget.ButtonClickedEventArgs) { + actionDispatcher.ToggleStats() + }), + ) + + topRightStatsC := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewRowLayout( + widget.RowLayoutOpts.Direction(widget.DirectionVertical), + widget.RowLayoutOpts.Spacing(20), + )), + widget.ContainerOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.RowLayoutData{ + Stretch: true, + }), + ), + ) + + entries := make([]any, 0, 0) + statsListW := widget.NewList( + // Set the entries in the list + widget.ListOpts.Entries(entries), + widget.ListOpts.ScrollContainerOpts( + // Set the background images/color for the list + widget.ScrollContainerOpts.Image(&widget.ScrollContainerImage{ + Idle: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + Disabled: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + Mask: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + }), + ), + widget.ListOpts.SliderOpts( + // Set the background images/color for the background of the slider track + widget.SliderOpts.Images(&widget.SliderTrackImage{ + Idle: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + Hover: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + }, buttonImage), + widget.SliderOpts.MinHandleSize(5), + // Set how wide the track should be + widget.SliderOpts.TrackPadding(widget.NewInsetsSimple(2)), + ), + // Hide the horizontal slider + widget.ListOpts.HideHorizontalSlider(), + widget.ListOpts.HideVerticalSlider(), + // Set the font for the list options + widget.ListOpts.EntryFontFace(smallFont), + // Set the colors for the list + widget.ListOpts.EntryColor(&widget.ListEntryColor{ + Selected: color.NRGBA{0, 255, 0, 255}, // Foreground color for the unfocused selected entry + Unselected: color.NRGBA{254, 255, 255, 255}, // Foreground color for the unfocused unselected entry + SelectedBackground: color.NRGBA{R: 130, G: 130, B: 200, A: 255}, // Background color for the unfocused selected entry + SelectedFocusedBackground: color.NRGBA{R: 130, G: 130, B: 170, A: 255}, // Background color for the focused selected entry + FocusedBackground: color.NRGBA{R: 170, G: 170, B: 180, A: 255}, // Background color for the focused unselected entry + DisabledUnselected: color.NRGBA{100, 100, 100, 255}, // Foreground color for the disabled unselected entry + DisabledSelected: color.NRGBA{100, 100, 100, 255}, // Foreground color for the disabled selected entry + DisabledSelectedBackground: color.NRGBA{100, 100, 100, 255}, // Background color for the disabled selected entry + }), + // This required function returns the string displayed in the list + widget.ListOpts.EntryLabelFunc(func(e interface{}) string { + return e.(string) + }), + // Padding for each entry + widget.ListOpts.EntryTextPadding(widget.NewInsetsSimple(5)), + // Text position for each entry + widget.ListOpts.EntryTextPosition(widget.TextPositionStart, widget.TextPositionCenter), + // This handler defines what function to run when a list item is selected. + widget.ListOpts.EntrySelectedHandler(func(args *widget.ListEntrySelectedEventArgs) { + //entry := args.Entry.(ListEntry) + //fmt.Println("Entry Selected: ", entry) + }), + ) + + incomeTextW := widget.NewText( + widget.TextOpts.Text("Gold: 40 Income Timer: 15s", smallFont, color.White), + widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter), + widget.TextOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.RowLayoutData{ + Position: widget.RowLayoutPositionStart, + }), + ), + ) + + bottomRightContainer := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + ) + + // Create the first tab + // A TabBookTab is a labelled container. The text here is what will show up in the tab button + tabUnits := widget.NewTabBookTab("UNITS", + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + widget.ContainerOpts.BackgroundImage(image.NewNineSliceColor(color.NRGBA{R: 100, G: 100, B: 120, A: 255})), + ) + + unitsC := widget.NewContainer( + // the container will use an anchor layout to layout its single child widget + widget.ContainerOpts.Layout(widget.NewGridLayout( + //Define number of columns in the grid + widget.GridLayoutOpts.Columns(5), + //Define how much padding to inset the child content + widget.GridLayoutOpts.Padding(widget.NewInsetsSimple(6)), + //Define how far apart the rows and columns should be + widget.GridLayoutOpts.Spacing(5, 5), + //Define how to stretch the rows and columns. Note it is required to + //specify the Stretch for each row and column. + widget.GridLayoutOpts.Stretch([]bool{false, false, false, false, false}, []bool{false, false, false, false, false}), + )), + ) + for _, u := range sortedUnits() { + ubtn := widget.NewButton( + // set general widget options + widget.ButtonOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.GridLayoutData{ + MaxWidth: 38, + MaxHeight: 38, + //Position: widget.RowLayoutPositionCenter, + //Stretch: false, + }), + ), + + // specify the images to sue + widget.ButtonOpts.Image(buttonImageFromImage(u.Faceset)), + + // add a handler that reacts to clicking the button + widget.ButtonOpts.ClickedHandler(func(u *unit.Unit) func(args *widget.ButtonClickedEventArgs) { + return func(args *widget.ButtonClickedEventArgs) { + cp := hs.game.Store.Players.FindCurrent() + actionDispatcher.SummonUnit(u.Type.String(), cp.ID, cp.LineID, hs.game.Store.Map.GetNextLineID(cp.LineID)) + } + }(u)), + ) + unitsC.AddChild(ubtn) + } + hs.unitsC = unitsC + tabUnits.AddChild(unitsC) + + tabTowers := widget.NewTabBookTab("TOWERS", + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + widget.ContainerOpts.BackgroundImage(image.NewNineSliceColor(color.NRGBA{R: 100, G: 100, B: 120, A: 255})), + ) + towersC := widget.NewContainer( + // the container will use an anchor layout to layout its single child widget + widget.ContainerOpts.Layout(widget.NewGridLayout( + //Define number of columns in the grid + widget.GridLayoutOpts.Columns(1), + //Define how much padding to inset the child content + widget.GridLayoutOpts.Padding(widget.NewInsetsSimple(6)), + //Define how far apart the rows and columns should be + widget.GridLayoutOpts.Spacing(5, 5), + //Define how to stretch the rows and columns. Note it is required to + //specify the Stretch for each row and column. + widget.GridLayoutOpts.Stretch([]bool{false, false, false, false, false}, []bool{false, false, false, false, false}), + )), + ) + for _, t := range sortedTowers() { + tbtn := widget.NewButton( + // set general widget options + widget.ButtonOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.GridLayoutData{ + MaxWidth: 38, + MaxHeight: 38, + //Position: widget.RowLayoutPositionCenter, + //Stretch: false, + }), + ), + + // specify the images to sue + widget.ButtonOpts.Image(buttonImageFromImage(t.Faceset)), + + // add a handler that reacts to clicking the button + widget.ButtonOpts.ClickedHandler(func(t *tower.Tower) func(args *widget.ButtonClickedEventArgs) { + return func(args *widget.ButtonClickedEventArgs) { + hst := hs.GetState().(HUDState) + actionDispatcher.SelectTower(t.Type.String(), int(hst.LastCursorPosition.X), int(hst.LastCursorPosition.Y)) + } + }(t)), + ) + towersC.AddChild(tbtn) + } + hs.towersC = towersC + tabTowers.AddChild(towersC) + + tabBook := widget.NewTabBook( + widget.TabBookOpts.TabButtonImage(buttonImage), + widget.TabBookOpts.TabButtonText(smallFont, &widget.ButtonTextColor{Idle: color.White, Disabled: color.White}), + widget.TabBookOpts.TabButtonSpacing(0), + widget.TabBookOpts.ContainerOpts( + widget.ContainerOpts.WidgetOpts(widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{ + HorizontalPosition: widget.AnchorLayoutPositionEnd, + VerticalPosition: widget.AnchorLayoutPositionEnd, + })), + ), + widget.TabBookOpts.TabButtonOpts( + widget.ButtonOpts.TextPadding(widget.NewInsetsSimple(5)), + widget.ButtonOpts.WidgetOpts(widget.WidgetOpts.MinSize(98, 0)), + ), + widget.TabBookOpts.Tabs(tabUnits, tabTowers), + ) + bottomRightContainer.AddChild(tabBook) + + hs.incomeTextW = incomeTextW + hs.statsListW = statsListW + + topRightStatsC.AddChild(incomeTextW) + topRightStatsC.AddChild(statsListW) + + topRightHorizontalRowC.AddChild(statsBtnW) + topRightHorizontalRowC.AddChild(homeBtnW) + topRightVerticalRowWraperC.AddChild(topRightHorizontalRowC) + topRightVerticalRowC.AddChild(topRightVerticalRowWraperC) + topRightVerticalRowC.AddChild(topRightStatsC) + topRightContainer.AddChild(topRightVerticalRowC) + + topLeftBtnContainer := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + ) + + leaveBtnW := widget.NewButton( + widget.ButtonOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{ + HorizontalPosition: widget.AnchorLayoutPositionStart, + VerticalPosition: widget.AnchorLayoutPositionStart, + }), + ), + + widget.ButtonOpts.Image(buttonImage), + + widget.ButtonOpts.Text("LEAVE", smallFont, &widget.ButtonTextColor{ + Idle: color.NRGBA{0xdf, 0xf4, 0xff, 0xff}, + }), + + widget.ButtonOpts.TextPadding(widget.Insets{ + Left: 30, + Right: 30, + Top: 5, + Bottom: 5, + }), + + widget.ButtonOpts.ClickedHandler(func(args *widget.ButtonClickedEventArgs) { + u := hs.game.Store.Players.FindCurrent() + actionDispatcher.RemovePlayer(u.ID) + }), + ) + topLeftBtnContainer.AddChild(leaveBtnW) + + centerTextContainer := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + ) + + winLoseTextW := widget.NewText( + widget.TextOpts.Text("", smallFont, color.White), + widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter), + widget.TextOpts.WidgetOpts( + widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{ + HorizontalPosition: widget.AnchorLayoutPositionCenter, + VerticalPosition: widget.AnchorLayoutPositionCenter, + }), + ), + ) + centerTextContainer.AddChild(winLoseTextW) + winLoseTextW.GetWidget().Visibility = widget.Visibility_Hide + hs.winLoseTextW = winLoseTextW + + rootContainer := widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewStackedLayout(widget.StackedLayoutOpts.Padding(widget.NewInsetsSimple(25)))), + ) + + rootContainer.AddChild(topRightContainer) + rootContainer.AddChild(topLeftBtnContainer) + rootContainer.AddChild(bottomRightContainer) + rootContainer.AddChild(centerTextContainer) + + hs.ui = &ebitenui.UI{ + Container: rootContainer, + } +} diff --git a/client/map.go b/client/map.go new file mode 100644 index 0000000..c22ae3c --- /dev/null +++ b/client/map.go @@ -0,0 +1,39 @@ +package client + +import ( + "image" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/xescugc/maze-wars/store" +) + +type Map struct { + game *Game +} + +func NewMap(g *Game) *Map { + m := &Map{ + game: g, + } + + return m +} + +func (m *Map) Update() error { + return nil +} + +func (m *Map) Draw(screen *ebiten.Image) { + // Draw will draw just a partial image of the map based on the viewport, so it does not render everything but just the + // part that it's seen by the user + // If we want to render everything and just move the viewport around we need o render the full image and change the + // opt.GeoM.Transport to the Map.X/Y and change the Update function to do the opposite in terms of -+ + // + // TODO: Maybe create a self Map entity with Update/Draw + op := &ebiten.DrawImageOptions{} + s := m.game.Camera.GetState().(CameraState) + op.GeoM.Scale(s.Zoom, s.Zoom) + inverseZoom := maxZoom - s.Zoom + zoomScale + mi := ebiten.NewImageFromImage(m.game.Store.Map.GetState().(store.MapState).Image) + screen.DrawImage(mi.SubImage(image.Rect(int(s.X), int(s.Y), int((s.X+s.W)*inverseZoom), int((s.Y+s.H)*inverseZoom))).(*ebiten.Image), op) +} diff --git a/client/router.go b/client/router.go index 7a53b64..4e89cf0 100644 --- a/client/router.go +++ b/client/router.go @@ -36,6 +36,7 @@ func NewRouterStore(d *flux.Dispatcher, su *SignUpStore, l *LobbyStore, wr *Wait rs.ReduceStore = flux.NewReduceStore(d, rs.Reduce, RouterState{ Route: SignUpRoute, + //Route: GameRoute, }) return rs diff --git a/client/sign_up.go b/client/sign_up.go index 97e542c..b99bf86 100644 --- a/client/sign_up.go +++ b/client/sign_up.go @@ -1,13 +1,15 @@ package client import ( + "image" "image/color" "github.com/ebitenui/ebitenui" - "github.com/ebitenui/ebitenui/image" + euiimage "github.com/ebitenui/ebitenui/image" "github.com/ebitenui/ebitenui/widget" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" "github.com/xescugc/go-flux" "github.com/xescugc/maze-wars/action" "github.com/xescugc/maze-wars/inputer" @@ -134,8 +136,8 @@ func (su *SignUpStore) buildUI() { //If the NineSlice image has a minimum size, the widget will sue that or // widget.WidgetOpts.MinSize; whichever is greater widget.TextInputOpts.Image(&widget.TextInputImage{ - Idle: image.NewNineSliceColor(color.NRGBA{R: 100, G: 100, B: 100, A: 255}), - Disabled: image.NewNineSliceColor(color.NRGBA{R: 100, G: 100, B: 100, A: 255}), + Idle: euiimage.NewNineSliceColor(color.NRGBA{R: 100, G: 100, B: 100, A: 255}), + Disabled: euiimage.NewNineSliceColor(color.NRGBA{R: 100, G: 100, B: 100, A: 255}), }), //Set the font face and size for the widget @@ -223,11 +225,11 @@ func (su *SignUpStore) buildUI() { } func loadButtonImage() (*widget.ButtonImage, error) { - idle := image.NewNineSliceColor(color.NRGBA{R: 170, G: 170, B: 180, A: 255}) + idle := euiimage.NewNineSliceColor(color.NRGBA{R: 170, G: 170, B: 180, A: 255}) - hover := image.NewNineSliceColor(color.NRGBA{R: 130, G: 130, B: 150, A: 255}) + hover := euiimage.NewNineSliceColor(color.NRGBA{R: 130, G: 130, B: 150, A: 255}) - pressed := image.NewNineSliceColor(color.NRGBA{R: 100, G: 100, B: 120, A: 255}) + pressed := euiimage.NewNineSliceColor(color.NRGBA{R: 100, G: 100, B: 120, A: 255}) return &widget.ButtonImage{ Idle: idle, @@ -235,3 +237,21 @@ func loadButtonImage() (*widget.ButtonImage, error) { Pressed: pressed, }, nil } + +func buttonImageFromImage(i image.Image) *widget.ButtonImage { + ei := ebiten.NewImageFromImage(i) + nsi := euiimage.NewNineSliceSimple(ei, i.Bounds().Dx(), i.Bounds().Dy()) + + dest := i + cm := colorm.ColorM{} + cm.Scale(2, 0.5, 0.5, 0.9) + edest := ebiten.NewImageFromImage(dest) + colorm.DrawImage(edest, ei, cm, nil) + dsi := euiimage.NewNineSliceSimple(edest, dest.Bounds().Dx(), dest.Bounds().Dy()) + return &widget.ButtonImage{ + Idle: nsi, + Hover: nsi, + Pressed: nsi, + Disabled: dsi, + } +} diff --git a/client/waiting_room.go b/client/waiting_room.go index ec1f126..5762e99 100644 --- a/client/waiting_room.go +++ b/client/waiting_room.go @@ -48,9 +48,6 @@ func (wr *WaitingRoomStore) Draw(screen *ebiten.Image) { wr.textPlayersW.Label = fmt.Sprintf("%d/%d", wrstate.TotalPlayers, wrstate.Size) wr.textColdownW.Label = fmt.Sprintf("(%ds to reduce the size, minimum is 2)", wrstate.Countdown) wr.ui.Draw(screen) - // TODO: - // Missing the EXIT - // Missing the START_GAME } func (wr *WaitingRoomStore) Reduce(state, a interface{}) interface{} { diff --git a/client/wasm/main.go b/client/wasm/main.go index f60e85d..bae0f4e 100644 --- a/client/wasm/main.go +++ b/client/wasm/main.go @@ -65,6 +65,8 @@ func NewClient() js.Func { return fmt.Errorf("failed to initialize HUDStore: %w", err) } + g.Map = client.NewMap(g) + us := client.NewUserStore(d) cls := client.NewStore(s, us) diff --git a/cmd/client/main.go b/cmd/client/main.go index feaa439..5c91c3d 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -60,6 +60,8 @@ var ( return fmt.Errorf("failed to initialize HUDStore: %w", err) } + g.Map = client.NewMap(g) + us := client.NewUserStore(d) cls := client.NewStore(s, us) @@ -85,7 +87,7 @@ var ( ) func init() { - clientCmd.Flags().StringVar(&hostURL, "port", "localhost:5555", "The URL of the server") + clientCmd.Flags().StringVar(&hostURL, "port", "http://localhost:5555", "The URL of the server") clientCmd.Flags().IntVar(&screenW, "screenw", 288, "The default width of the screen when not full screen") clientCmd.Flags().IntVar(&screenH, "screenh", 240, "The default height of the screen when not full screen") clientCmd.Flags().BoolVar(&verbose, "verbose", false, "Logs information of the running client") diff --git a/cmd/server/main.go b/cmd/server/main.go index 963833c..0a50653 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -19,7 +19,7 @@ var ( d := flux.NewDispatcher() ad := server.NewActionDispatcher(d, ss) rooms := server.NewRoomsStore(d, ss) - users := server.NewUsersStore(d) + users := server.NewUsersStore(d, ss) ss.Rooms = rooms ss.Users = users diff --git a/server/action.go b/server/action.go index 8a0187b..3d22b04 100644 --- a/server/action.go +++ b/server/action.go @@ -31,30 +31,27 @@ func NewActionDispatcher(d *flux.Dispatcher, s *Store) *ActionDispatcher { func (ac *ActionDispatcher) Dispatch(a *action.Action) { switch a.Type { case action.JoinWaitingRoom: - rstate := ac.store.Rooms.GetState().(RoomsState) - oldwr := rstate.CurrentWaitingRoom - ac.dispatcher.Dispatch(a) - rstate = ac.store.Rooms.GetState().(RoomsState) - // The only possibility for the CWR to be "" is that it has - // reached the full size, so we need to start the game - if rstate.CurrentWaitingRoom == "" && oldwr != "" { - ac.startGame(oldwr) - } + ac.startGame() default: ac.dispatcher.Dispatch(a) } } -func (ac *ActionDispatcher) startGame(oldwr string) { +func (ac *ActionDispatcher) startGame() { + wr := ac.store.Rooms.FindCurrentWaitingRoom() + if wr == nil || (len(wr.Players) != wr.Size) { + return + } + rstate := ac.store.Rooms.GetState().(RoomsState) - sga := action.NewStartGame(oldwr) + sga := action.NewStartGame() ac.dispatcher.Dispatch(sga) ac.UpdateState(ac.store.Rooms) - for _, p := range rstate.Rooms[oldwr].Players { + for _, p := range rstate.Rooms[wr.Name].Players { err := wsjson.Write(context.Background(), p.Conn, sga) if err != nil { log.Fatal(err) @@ -62,30 +59,16 @@ func (ac *ActionDispatcher) startGame(oldwr string) { } } -func (ac *ActionDispatcher) RemovePlayer(rn, sid string) { - rpa := action.NewRemovePlayer(rn, sid) - rpa.Room = rn - ac.dispatcher.Dispatch(rpa) -} - func (ac *ActionDispatcher) IncomeTick(rooms *RoomsStore) { ita := action.NewIncomeTick() ac.dispatcher.Dispatch(ita) } func (ac *ActionDispatcher) WaitRoomCountdownTick() { - rstate := ac.store.Rooms.GetState().(RoomsState) - oldwr := rstate.CurrentWaitingRoom - wrcta := action.NewWaitRoomCountdownTick() ac.dispatcher.Dispatch(wrcta) - rstate = ac.store.Rooms.GetState().(RoomsState) - // The only possibility for the CWR to be "" is that it has - // reached the full size, so we need to start the game - if rstate.CurrentWaitingRoom == "" && oldwr != "" { - ac.startGame(oldwr) - } + ac.startGame() } func (ac *ActionDispatcher) TPS(rooms *RoomsStore) { diff --git a/server/assets/wasm/maze-wars.wasm b/server/assets/wasm/maze-wars.wasm index 1aaa743..aaad5c5 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 9c57e02..8a03e71 100644 --- a/server/new.go +++ b/server/new.go @@ -108,6 +108,12 @@ func usersCreateHandler(s *Store) func(http.ResponseWriter, *http.Request) { return } + if _, ok := s.Users.FindByRemoteAddress(r.RemoteAddr); ok { + w.WriteHeader(http.StatusBadRequest) + json.NewEncoder(w).Encode(errorResponse{Error: "A session already exists from this computer"}) + return + } + actionDispatcher.UserSignUp(ucr.Username) w.Header().Set("Content-Type", "application/json") @@ -126,23 +132,22 @@ func wsHandler(s *Store) func(http.ResponseWriter, *http.Request) { // we kick the user err := wsjson.Read(hr.Context(), ws, &msg) if err != nil { + // We cannot move this 'u' call outside as the Read + // block until a new message is received so it may have + // a wrong value stored inside + u, _ := s.Users.FindByRemoteAddress(hr.RemoteAddr) fmt.Printf("Error when reading the WS message: %s\n", err) - u, ok := s.Users.FindByRemoteAddress(hr.RemoteAddr) - if ok { - actionDispatcher.UserSignOut(u.Username) - } - - rstate := s.Rooms.GetState().(RoomsState) - for rn, r := range rstate.Rooms { - if uid, ok := r.Connections[hr.RemoteAddr]; ok { - actionDispatcher.RemovePlayer(rn, uid) - break - } - } + actionDispatcher.UserSignOut(u.Username) break } + u, _ := s.Users.FindByRemoteAddress(hr.RemoteAddr) + + // If the User is in a Room we set it directly on the + // action from the handler + msg.Room = u.CurrentRoomID + switch msg.Type { case action.UserSignIn: // We need to append this extra information to the Action diff --git a/server/rooms.go b/server/rooms.go index a6ba3c0..ad63a94 100644 --- a/server/rooms.go +++ b/server/rooms.go @@ -56,14 +56,26 @@ func (rs *RoomsStore) List() []*Room { rs.mxRooms.RLock() defer rs.mxRooms.RUnlock() - mrooms := rs.GetState().(RoomsState) - rooms := make([]*Room, 0, len(mrooms.Rooms)) - for _, r := range mrooms.Rooms { + srooms := rs.GetState().(RoomsState) + rooms := make([]*Room, 0, len(srooms.Rooms)) + for _, r := range srooms.Rooms { rooms = append(rooms, r) } return rooms } +func (rs *RoomsStore) FindCurrentWaitingRoom() *Room { + rs.mxRooms.RLock() + defer rs.mxRooms.RUnlock() + + srooms := rs.GetState().(RoomsState) + r, ok := srooms.Rooms[srooms.CurrentWaitingRoom] + if !ok { + return nil + } + return r +} + func (rs *RoomsStore) GetNextID(room string) int { r, _ := rs.GetState().(RoomsState).Rooms[room] return len(r.Players) @@ -82,29 +94,31 @@ func (rs *RoomsStore) Reduce(state, a interface{}) interface{} { switch act.Type { case action.StartGame: + rs.GetDispatcher().WaitFor(rs.Store.Users.GetDispatcherToken()) + rd := flux.NewDispatcher() g := NewGame(rd) - rstate.Rooms[act.StartGame.Room].Game = g - // TODO: + rstate.Rooms[rstate.CurrentWaitingRoom].Game = g pcount := 0 - for pid, pc := range rstate.Rooms[act.StartGame.Room].Players { + for pid, pc := range rstate.Rooms[rstate.CurrentWaitingRoom].Players { u, _ := rs.Store.Users.FindByRemoteAddress(pc.RemoteAddr) g.Dispatch(action.NewAddPlayer(pid, u.Username, pcount)) pcount++ } + rstate.CurrentWaitingRoom = "" case action.RemovePlayer: rs.mxRooms.Lock() defer rs.mxRooms.Unlock() - pc := rstate.Rooms[act.RemovePlayer.Room].Players[act.RemovePlayer.ID] - delete(rstate.Rooms[act.RemovePlayer.Room].Players, act.RemovePlayer.ID) - delete(rstate.Rooms[act.RemovePlayer.Room].Connections, pc.RemoteAddr) - - rstate.Rooms[act.Room].Game.Dispatch(act) + removePlayer(&rstate, act.RemovePlayer.ID, act.Room) + case action.UserSignOut: + rs.mxRooms.Lock() + defer rs.mxRooms.Unlock() - if len(rstate.Rooms[act.Room].Players) == 0 { - delete(rstate.Rooms, act.Room) + u, ok := rs.Store.Users.FindByUsername(act.UserSignOut.Username) + if ok && u.CurrentRoomID != "" { + removePlayer(&rstate, u.ID, u.CurrentRoomID) } case action.JoinWaitingRoom: rs.mxRooms.Lock() @@ -117,7 +131,7 @@ func (rs *RoomsStore) Reduce(state, a interface{}) interface{} { Players: make(map[string]PlayerConn), Connections: make(map[string]string), - Size: 6, + Size: 2, Countdown: 10, } rstate.CurrentWaitingRoom = rid.String() @@ -130,12 +144,6 @@ func (rs *RoomsStore) Reduce(state, a interface{}) interface{} { RemoteAddr: us.RemoteAddr, } wr.Connections[us.RemoteAddr] = us.ID - - if len(wr.Players) == wr.Size { - // As the size has been reached we remove - // the current room as WR - rstate.CurrentWaitingRoom = "" - } case action.WaitRoomCountdownTick: rs.mxRooms.Lock() defer rs.mxRooms.Unlock() @@ -154,10 +162,6 @@ func (rs *RoomsStore) Reduce(state, a interface{}) interface{} { wr.Countdown = 0 } } - - if wr.Size == len(wr.Players) { - rstate.CurrentWaitingRoom = "" - } case action.ExitWaitingRoom: rs.mxRooms.Lock() defer rs.mxRooms.Unlock() @@ -190,3 +194,15 @@ func (rs *RoomsStore) Reduce(state, a interface{}) interface{} { return rstate } + +func removePlayer(rstate *RoomsState, pid, room string) { + pc := rstate.Rooms[room].Players[pid] + delete(rstate.Rooms[room].Players, pid) + delete(rstate.Rooms[room].Connections, pc.RemoteAddr) + + rstate.Rooms[room].Game.Dispatch(action.NewRemovePlayer(pid)) + + if len(rstate.Rooms[room].Players) == 0 { + delete(rstate.Rooms, room) + } +} diff --git a/server/users.go b/server/users.go index 47454b5..16a7f45 100644 --- a/server/users.go +++ b/server/users.go @@ -27,10 +27,14 @@ type User struct { Conn *websocket.Conn RemoteAddr string + + CurrentRoomID string } -func NewUsersStore(d *flux.Dispatcher) *UsersStore { - us := &UsersStore{} +func NewUsersStore(d *flux.Dispatcher, s *Store) *UsersStore { + us := &UsersStore{ + Store: s, + } us.ReduceStore = flux.NewReduceStore(d, us.Reduce, UsersState{ Users: make(map[string]*User), @@ -105,10 +109,37 @@ func (us *UsersStore) Reduce(state, a interface{}) interface{} { u.RemoteAddr = act.UserSignIn.RemoteAddr } case action.UserSignOut: + us.GetDispatcher().WaitFor(us.Store.Rooms.GetDispatcherToken()) + us.mxUsers.Lock() defer us.mxUsers.Unlock() delete(ustate.Users, act.UserSignOut.Username) + case action.StartGame: + us.mxUsers.Lock() + defer us.mxUsers.Unlock() + + r := us.Store.Rooms.FindCurrentWaitingRoom() + if r != nil { + for pid := range r.Players { + for _, u := range ustate.Users { + if u.ID == pid { + u.CurrentRoomID = r.Name + } + } + } + } + + case action.RemovePlayer: + us.mxUsers.Lock() + defer us.mxUsers.Unlock() + + for _, u := range ustate.Users { + if u.ID == act.RemovePlayer.ID { + u.CurrentRoomID = "" + break + } + } } return ustate diff --git a/store/map.go b/store/map.go index a9fe7b0..5a8d3a0 100644 --- a/store/map.go +++ b/store/map.go @@ -166,27 +166,10 @@ func (m *Map) Reduce(state, a interface{}) interface{} { switch act.Type { case action.StartGame: - players := m.store.Players.List() - if len(players) > 1 { - allReady := true - for _, p := range players { - if !p.Ready { - allReady = false - break - } - } - // TODO: This action could be done from the NavigateTo from the - // lobby but we need the server to do the same logic and it does - // not make sense to send NavigateTo events to the server. - // - // If all players are ready then the map must be set - if allReady { - mstate.Players = len(players) - mstate.Image, ok = mapImages[mstate.Players] - if !ok { - log.Fatalf("The map for the number of players %d is not available", mstate.Players) - } - } + mstate.Players = len(m.store.Players.List()) + mstate.Image, ok = mapImages[mstate.Players] + if !ok { + log.Fatalf("The map for the number of players %d is not available", mstate.Players) } } diff --git a/store/players.go b/store/players.go index fee9f38..829d5e9 100644 --- a/store/players.go +++ b/store/players.go @@ -37,7 +37,6 @@ type Player struct { Gold int Current bool Winner bool - Ready bool } func (p Player) CanSummonUnit(ut string) bool { @@ -144,6 +143,14 @@ func (ps *Players) Reduce(state, a interface{}) interface{} { defer ps.mxPlayers.Unlock() delete(pstate.Players, act.RemovePlayer.ID) + + if len(pstate.Players) == 1 { + for _, p := range pstate.Players { + // As there is only 1 we can do it this way + p.Winner = true + } + } + case action.StealLive: ps.mxPlayers.Lock() defer ps.mxPlayers.Unlock() @@ -213,11 +220,6 @@ func (ps *Players) Reduce(state, a interface{}) interface{} { defer ps.mxPlayers.Unlock() pstate.Players[act.RemoveTower.PlayerID].Gold += tower.Towers[act.RemoveTower.TowerType].Gold / 2 - case action.PlayerReady: - ps.mxPlayers.Lock() - defer ps.mxPlayers.Unlock() - - pstate.Players[act.PlayerReady.ID].Ready = true case action.UnitKilled: ps.mxPlayers.Lock() defer ps.mxPlayers.Unlock()