diff --git a/src/net/local_server.go b/src/net/local_server.go index 9229a90..013bee1 100644 --- a/src/net/local_server.go +++ b/src/net/local_server.go @@ -20,7 +20,7 @@ var localGame = struct { // StartLocalGame starts a private one-player server in this process. The // multiplayer socket is suspended until StopLocalGame is called. -func StartLocalGame(seed int64) error { +func StartLocalGame() error { if game.PlayerData == nil { return fmt.Errorf("start local game: player data is not loaded") } @@ -46,7 +46,6 @@ func StartLocalGame(seed int64) error { if err := transport.send(&packets.C2SCreateGamePacket{ Public: false, MaxPlayers: 1, - Seed: seed, }); err != nil { stopLocalGame(true) return fmt.Errorf("create local game: %w", err) diff --git a/src/net/packets/game_lifecycle.go b/src/net/packets/game_lifecycle.go index 585c639..8d78396 100644 --- a/src/net/packets/game_lifecycle.go +++ b/src/net/packets/game_lifecycle.go @@ -5,7 +5,6 @@ import "github.com/threeidiotsonegamejam/gmtk26/src/game" type C2SCreateGamePacket struct { Public bool `json:"public"` MaxPlayers uint8 `json:"max_players"` - Seed int64 `json:"seed"` } type C2SJoinGamePacket struct { diff --git a/src/server/client.go b/src/server/client.go index deedc00..b31bd5d 100644 --- a/src/server/client.go +++ b/src/server/client.go @@ -187,7 +187,7 @@ func (c *Client) handleCreateGamePacket(packet *packets.C2SCreateGamePacket) (pa return nil, fatalPacketErrorf("handle create game packet: client is not ready") } - state, err := Lobbies.CreateGame(c, packet.Public, packet.MaxPlayers, packet.Seed) + state, err := Lobbies.CreateGame(c, packet.Public, packet.MaxPlayers) if err != nil { return &packets.S2CGameRejectedPacket{ Operation: "create", diff --git a/src/server/lobby_manager.go b/src/server/lobby_manager.go index e15ddb0..e0cb2c0 100644 --- a/src/server/lobby_manager.go +++ b/src/server/lobby_manager.go @@ -2,6 +2,7 @@ package server import ( "crypto/rand" + "encoding/binary" "errors" "fmt" "log" @@ -66,7 +67,7 @@ func NewLobbyManager() *LobbyManager { } } -func (m *LobbyManager) CreateGame(client *Client, public bool, maxPlayers uint8, seed int64) (game.Game, error) { +func (m *LobbyManager) CreateGame(client *Client, public bool, maxPlayers uint8) (game.Game, error) { if maxPlayers < 1 || maxPlayers > 4 { return game.Game{}, fmt.Errorf("max players must be 1, 2, 3, or 4") } @@ -104,9 +105,6 @@ func (m *LobbyManager) CreateGame(client *Client, public bool, maxPlayers uint8, Multiplayer: maxPlayers > 1, MaxPlayers: maxPlayers, Round: 1, - Map: game.Map{ - Seed: seed, - }, } state.Factions[0].Player = playerPointer(player) for i := int(maxPlayers); i < len(state.Factions); i++ { @@ -215,6 +213,13 @@ func (m *LobbyManager) StartGame(client *Client) error { return ErrNotEnoughPlayers } + seed, err := newGameSeed() + if err != nil { + m.mu.Unlock() + return err + } + active.state.Map.Seed = seed + delete(m.lobbies, gameID) delete(m.gameCodes, active.state.GameCode) for _, c := range active.clients { @@ -459,6 +464,14 @@ func (m *LobbyManager) newGameCodeLocked() (string, error) { return "", fmt.Errorf("generate game code: no unique code available") } +func newGameSeed() (int64, error) { + var bytes [8]byte + if _, err := rand.Read(bytes[:]); err != nil { + return 0, fmt.Errorf("generate game seed: %w", err) + } + return int64(binary.LittleEndian.Uint64(bytes[:])), nil +} + func availableFaction(state game.Game) int { factionCount := min(int(state.MaxPlayers), len(state.Factions)) for i := range factionCount { diff --git a/src/ui/game_world_element.go b/src/ui/game_world_element.go index 0452779..85dcb2e 100644 --- a/src/ui/game_world_element.go +++ b/src/ui/game_world_element.go @@ -17,15 +17,17 @@ func GameWorld() *GameWorldElement { type GameWorldElement struct { BaseElement[*GameWorldElement] - Map game.Map - Renderer render.WorldRenderer + Map game.Map + Renderer render.WorldRenderer + pendingFocus *game.Hex } func (el *GameWorldElement) prepare() { - if el.Map.Grid == nil { - el.Map.Generate() - } el.Renderer.Init(&el.Map) + if el.pendingFocus != nil { + el.Renderer.FocusOnHex(*el.pendingFocus) + el.pendingFocus = nil + } } func (el *GameWorldElement) update(deltaNano int64) { @@ -36,3 +38,9 @@ func (el *GameWorldElement) update(deltaNano int64) { func (el *GameWorldElement) draw() { el.Renderer.Draw(&el.Map) } + +// FocusOnHex defers camera focus until prepare has initialized the renderer. +// This matters when the world stays hidden throughout the lobby. +func (el *GameWorldElement) FocusOnHex(hex game.Hex) { + el.pendingFocus = &hex +} diff --git a/src/ui/screens/game_creation_screen.go b/src/ui/screens/game_creation_screen.go index 66a8276..0b98d53 100644 --- a/src/ui/screens/game_creation_screen.go +++ b/src/ui/screens/game_creation_screen.go @@ -2,8 +2,6 @@ package screens import ( "fmt" - "hash/fnv" - "math/rand" "strconv" gameNet "github.com/threeidiotsonegamejam/gmtk26/src/net" @@ -28,7 +26,6 @@ var ( creationPublic = true creationSubmitting bool creationError string - creationSeedInput *ui.InputElement ) func OpenSoloGameCreation(previousScreen *ui.ScreenElement) { @@ -50,11 +47,11 @@ func RejectGameCreation(message string) { } func StartSoloWithDefaults() error { - return startSoloGame(rand.Int63()) + return startSoloGame() } func HostGameWithDefaults() error { - return sendHostGame(true, 4, rand.Int63()) + return sendHostGame(true, 4) } func openGameCreation(mode gameCreationMode, previousScreen *ui.ScreenElement) { @@ -64,7 +61,6 @@ func openGameCreation(mode gameCreationMode, previousScreen *ui.ScreenElement) { creationSubmitting = false creationError = "" screen := NewGameCreationScreen(previousScreen) - creationSeedInput.SetText(strconv.FormatInt(rand.Int63(), 10)) SetActiveScreen(screen) } @@ -83,37 +79,6 @@ func NewGameCreationScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { } }) - creationSeedInput = ui.Input(). - WithPlaceholderText("Seed or phrase"). - WithMaxTextLength(48). - WithDefaultText(""). - WithTextSize(30). - WithPadding(10). - WithSize(vec.Vec2i{X: 360, Y: 54}). - WithAnchors(anchor.Center, anchor.Center). - WithRelativePos(vec.Vec2i{X: -82, Y: -72}). - WithEnabledDynamic(func(el *ui.InputElement) bool { - return !creationSubmitting - }). - WithCallback(func(text string) { - creationError = "" - }) - - randomSeedButton := ui.Button(). - WithText("Random"). - WithTextSize(28). - WithPadding(8). - WithSize(vec.Vec2i{X: 150, Y: 54}). - WithAnchors(anchor.Center, anchor.Center). - WithRelativePos(vec.Vec2i{X: 205, Y: -72}). - WithEnabledDynamic(func(el *ui.ButtonElement) bool { - return !creationSubmitting - }). - WithClick(func() { - creationSeedInput.SetText(strconv.FormatInt(rand.Int63(), 10)) - creationError = "" - }) - playerCountButtons := make([]ui.Element, 0, 3) for i := range 3 { count := uint8(i + 2) @@ -128,7 +93,7 @@ func NewGameCreationScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { WithPadding(8). WithSize(vec.Vec2i{X: 100, Y: 52}). WithAnchors(anchor.Center, anchor.Center). - WithRelativePos(vec.Vec2i{X: int32(i-1) * 120, Y: 34}). + WithRelativePos(vec.Vec2i{X: int32(i-1) * 120, Y: -22}). WithVisibleDynamic(func(el *ui.ButtonElement) bool { return hostMode() }). @@ -152,7 +117,7 @@ func NewGameCreationScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { WithPadding(8). WithSize(vec.Vec2i{X: 360, Y: 52}). WithAnchors(anchor.Center, anchor.Center). - WithRelativePos(vec.Vec2i{X: 0, Y: 104}). + WithRelativePos(vec.Vec2i{X: 0, Y: 48}). WithVisibleDynamic(func(el *ui.ButtonElement) bool { return hostMode() }). @@ -182,9 +147,9 @@ func NewGameCreationScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { WithAnchors(anchor.Center, anchor.Center). WithRelativePosDynamic(func(el *ui.ButtonElement) vec.Vec2i { if hostMode() { - return vec.Vec2i{X: 0, Y: 170} + return vec.Vec2i{X: 0, Y: 114} } - return vec.Vec2i{X: 0, Y: 82} + return vec.Vec2i{X: 0, Y: 48} }). WithEnabledDynamic(func(el *ui.ButtonElement) bool { return !creationSubmitting && (!hostMode() || hostConnected()) @@ -212,30 +177,20 @@ func NewGameCreationScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { if hostMode() { return "Choose the rules" } - return "Choose a world seed before starting" + return "The world is generated when the game starts" }). WithTextSize(26). WithTextColor(uiutil.MenuMutedColor). WithAnchors(anchor.Center, anchor.Top). WithRelativePos(vec.Vec2i{X: 0, Y: 122}), ). - AddChild( - ui.Text(). - WithText("World Seed"). - WithTextSize(25). - WithTextColor(uiutil.MenuMutedColor). - WithAnchors(anchor.Center, anchor.Center). - WithRelativePos(vec.Vec2i{X: 0, Y: -116}), - ). - AddChild(creationSeedInput). - AddChild(randomSeedButton). AddChild( ui.Text(). WithText("Maximum Players"). WithTextSize(25). WithTextColor(uiutil.MenuMutedColor). WithAnchors(anchor.Center, anchor.Center). - WithRelativePos(vec.Vec2i{X: 0, Y: -12}). + WithRelativePos(vec.Vec2i{X: 0, Y: -68}). WithVisibleDynamic(func(el *ui.TextElement) bool { return hostMode() }), @@ -246,7 +201,7 @@ func NewGameCreationScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { WithTextSize(26). WithTextColor(uiutil.MenuMutedColor). WithAnchors(anchor.Center, anchor.Center). - WithRelativePos(vec.Vec2i{X: 0, Y: 10}). + WithRelativePos(vec.Vec2i{X: 0, Y: -18}). WithVisibleDynamic(func(el *ui.TextElement) bool { return !hostMode() }), @@ -281,18 +236,14 @@ func NewGameCreationScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { }), ). AddChild(uiutil.MenuVignette()). - WithBack(goBack). - WithExit(func() { - creationSeedInput.Blur() - }) + WithBack(goBack) } func submitGameCreation() { - seed := gameSeedFromText(creationSeedInput.Value()) creationError = "" if creationMode == gameCreationSolo { - if err := startSoloGame(seed); err != nil { + if err := startSoloGame(); err != nil { creationError = capitalizeSentence(err.Error()) return } @@ -300,25 +251,24 @@ func submitGameCreation() { return } - if err := sendHostGame(creationPublic, creationMaxPlayers, seed); err != nil { + if err := sendHostGame(creationPublic, creationMaxPlayers); err != nil { creationError = capitalizeSentence(err.Error()) return } creationSubmitting = true } -func startSoloGame(seed int64) error { - return gameNet.StartLocalGame(seed) +func startSoloGame() error { + return gameNet.StartLocalGame() } -func sendHostGame(public bool, maxPlayers uint8, seed int64) error { +func sendHostGame(public bool, maxPlayers uint8) error { if settings.Current.Offline || gameNet.State() != gameNet.ConnectionConnected { return fmt.Errorf("Connect to the multiplayer server before creating a game") } if err := gameNet.Send(&packets.C2SCreateGamePacket{ Public: public, MaxPlayers: maxPlayers, - Seed: seed, }); err != nil { return err } @@ -334,16 +284,3 @@ func gameCreationStatus() string { } return "" } - -func gameSeedFromText(text string) int64 { - if text == "" || text == "0" { - return 0 - } - if seed, err := strconv.ParseInt(text, 10, 64); err == nil { - return seed - } - - hash := fnv.New64a() - _, _ = hash.Write([]byte(text)) - return int64(hash.Sum64()) -} diff --git a/src/ui/screens/game_screen.go b/src/ui/screens/game_screen.go index 303bd66..c6c542b 100644 --- a/src/ui/screens/game_screen.go +++ b/src/ui/screens/game_screen.go @@ -4,7 +4,6 @@ import ( "fmt" "image/color" "math" - "math/rand" "strconv" "strings" "time" @@ -14,21 +13,19 @@ import ( rl "github.com/gen2brain/raylib-go/raylib" "github.com/threeidiotsonegamejam/gmtk26/src/audio" "github.com/threeidiotsonegamejam/gmtk26/src/game" - "github.com/threeidiotsonegamejam/gmtk26/src/global" gameNet "github.com/threeidiotsonegamejam/gmtk26/src/net" "github.com/threeidiotsonegamejam/gmtk26/src/net/packets" "github.com/threeidiotsonegamejam/gmtk26/src/render" "github.com/threeidiotsonegamejam/gmtk26/src/settings" "github.com/threeidiotsonegamejam/gmtk26/src/ui" "github.com/threeidiotsonegamejam/gmtk26/src/ui/anchor" + "github.com/threeidiotsonegamejam/gmtk26/src/ui/uiutil" "github.com/threeidiotsonegamejam/gmtk26/src/util/vec" ) const gameCodeCopyFeedbackDuration = 1500 * time.Millisecond -var gameSeedInput = ui.Input() var gameWorld = ui.GameWorld() -var gameRegenerateButton = ui.Button() var currentGame *game.Game var gamePreviousScreen *ui.ScreenElement var gameLeaveTransition bool @@ -40,6 +37,10 @@ var localClientID game.ClientID // serverGameActive is true between S2CGameStartPacket and S2CGameEndPacket // for both remote multiplayer and the in-process solo server. var serverGameActive bool + +// serverGameStarted is kept separate so the authoritative world remains +// visible on the game-over screen while staying hidden throughout the lobby. +var serverGameStarted bool var serverRound int32 var serverCoins int32 var serverPoints int32 @@ -156,6 +157,7 @@ func ApplyServerGameStart(p *packets.S2CGameStartPacket) { return } serverGameActive = true + serverGameStarted = true gameOverMessage = "" clearRoundAnnouncement() gameWorld.Renderer.ClearQueuedBuilding() @@ -252,7 +254,6 @@ func applyServerRound( key := fmt.Sprintf("%d:%d:%d:%t", result.Round, result.Type, result.Status, result.Automatic) showResolutionToast(result.Message, key) } - gameSeedInput.SetText(strconv.FormatInt(m.Seed, 10)) } func showResolutionToast(message, key string) { @@ -436,6 +437,7 @@ func newRoundCountdown() *ui.GroupElement { func EnterGame(state game.Game) { clearMatchmaking() serverGameActive = false + serverGameStarted = false clearRoundAnnouncement() focusTownhallPending = false gameOverMessage = "" @@ -445,7 +447,9 @@ func EnterGame(state game.Game) { gameWorld.Renderer.ClearSelection() gameWorld.Renderer.ActionsEnabled = false applyGameState(state) - gameWorld.Renderer.ResetCamera(&gameWorld.Map) + if serverGameStarted { + gameWorld.Renderer.ResetCamera(&gameWorld.Map) + } if screenIsActiveOrPending(gameScreen) { return } @@ -529,6 +533,14 @@ func LeaveCurrentGame() { } func applyGameState(state game.Game) { + if !serverGameStarted { + // Lobby updates intentionally contain no world data. Keep the renderer + // empty until the authoritative start packet arrives. + currentGame = &state + gameWorld.Map = game.Map{} + return + } + nextMap := state.Map if currentGame != nil && currentGame.GameID == state.GameID && @@ -542,7 +554,6 @@ func applyGameState(state game.Game) { state.Map = nextMap gameWorld.Map = nextMap - gameSeedInput.SetText(strconv.FormatInt(nextMap.Seed, 10)) currentGame = &state } @@ -553,6 +564,7 @@ func clearCurrentGame() { gameWorld.Renderer.ClearSelection() gameLeaveTransition = false serverGameActive = false + serverGameStarted = false serverGameEndTime = 0 clearRoundAnnouncement() focusTownhallPending = false @@ -801,9 +813,12 @@ func NewGameScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { }) screen := ui.Screen(). + WithBackgroundColor(uiutil.MenuScreenBackground). WithEnter(func() { HideEscScreen() - gameWorld.Renderer.ResetCamera(&gameWorld.Map) + if serverGameStarted { + gameWorld.Renderer.ResetCamera(&gameWorld.Map) + } audio.StartMusic() audio.StartAmbience() @@ -825,7 +840,12 @@ func NewGameScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { }), ). AddChild( - gameWorld, + uiutil.MenuBackdrop(), + ). + AddChild( + gameWorld.WithVisibleDynamic(func(el *ui.GameWorldElement) bool { + return serverGameStarted + }), ). AddChild(gameCodeButton). AddChild( @@ -905,44 +925,6 @@ func NewGameScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { }), ). AddChild(buildingToolbar()). - AddChild( - ui.Group(). - WithAnchors(anchor.TopLeft, anchor.TopLeft). - WithRelativePos(vec.Vec2i{X: 8, Y: 8}). - WithVisibleDynamic(func(el *ui.GroupElement) bool { - return !serverGameActive && global.DebugEnabled - }). - AddChild( - gameSeedInput. - WithPadding(8). - WithTextSize(24). - WithSize(vec.Vec2i{X: 320, Y: 0}). - WithPlaceholderText("Seed"). - WithDefaultText(""), - ). - AddChild( - gameRegenerateButton. - WithPadding(8). - WithTextSize(24). - WithRelativePos(vec.Vec2i{X: 0, Y: 52}). - WithText("Regenerate"). - WithClick(func() { - gameWorld.Map.Seed = gameSeedFromText(gameSeedInput.Value()) - gameWorld.Map.Generate() - }), - ). - AddChild( - ui.Button(). - WithPadding(8). - WithTextSize(24). - WithRelativePos(vec.Vec2i{X: 0, Y: 104}). - WithText("Random"). - WithClick(func() { - gameSeedInput.SetText(strconv.FormatInt(rand.Int63(), 10)) - gameRegenerateButton.Click() - }), - ), - ). AddChild(serverResourceList()). AddChild( ui.Text(). @@ -997,7 +979,10 @@ func NewGameScreen(previousScreen *ui.ScreenElement) *ui.ScreenElement { ). AddChild( ui.GameBuildingDetailsPanel(). - WithWorld(gameWorld), + WithWorld(gameWorld). + WithVisibleDynamic(func(el *ui.GameBuildingDetailsPanelElement) bool { + return serverGameStarted + }), ). AddChild( ui.Vignette().WithAlpha(120),