From 2fc5a62be609bd35579a79382190a777baa732ea Mon Sep 17 00:00:00 2001 From: jchv Date: Tue, 4 Jul 2023 18:22:35 -0400 Subject: [PATCH] More gameplay improvements (#4) * Correctly handle hole progression on room creation * Handle players crashing/leaving gracefully. * If game would end after player leaves, don't crash * Add experience points. * Fake EXP support, some state update fixes. * Fix reverse migration. --- database/accounts/service.go | 32 ++++++++++++ game/model/room.go | 1 + game/packet/client.go | 32 +++++++----- game/packet/server.go | 8 ++- game/room/event.go | 8 +++ game/room/room.go | 85 ++++++++++++++++++++++++++++---- game/server/conn.go | 69 +++++++++++++++++++++----- game/server/playerdata.go | 5 +- game/server/server.go | 3 +- gen/dbmodels/models.go | 1 + gen/dbmodels/player.sql.go | 69 ++++++++++++++++++++++---- migrations/0002_exp.sql | 5 ++ pangya/rank.go | 94 ++++++++++++++++++++++++++++++++++++ queries/player.sql | 8 ++- 14 files changed, 372 insertions(+), 48 deletions(-) create mode 100644 migrations/0002_exp.sql diff --git a/database/accounts/service.go b/database/accounts/service.go index f8ffbde..623c258 100755 --- a/database/accounts/service.go +++ b/database/accounts/service.go @@ -911,3 +911,35 @@ func (s *Service) DeleteExpiredSessions(ctx context.Context) error { func (s *Service) GetPlayerInventory(ctx context.Context, playerID int64) ([]dbmodels.Inventory, error) { return s.queries.GetPlayerInventory(ctx, playerID) } + +func (s *Service) AddExp(ctx context.Context, playerID int64, add int) (pangya.Rank, int, error) { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return 0, 0, err + } + defer tx.Rollback() + + queries := s.queries.WithTx(tx) + + rank, err := queries.GetPlayerRank(ctx, playerID) + if err != nil { + return 0, 0, err + } + + newRank, newExp := pangya.AddExperience(pangya.Rank(rank.Rank), int(rank.Exp), add) + values, err := queries.SetPlayerRank(ctx, dbmodels.SetPlayerRankParams{ + PlayerID: playerID, + Rank: int64(newRank), + Exp: int64(newExp), + }) + if err != nil { + return 0, 0, err + } + + err = tx.Commit() + if err != nil { + return 0, 0, err + } + + return pangya.Rank(values.Rank), int(values.Exp), nil +} diff --git a/game/model/room.go b/game/model/room.go index cfd60d0..bf90f2f 100644 --- a/game/model/room.go +++ b/game/model/room.go @@ -119,6 +119,7 @@ type RoomState struct { OwnerConnID uint32 NaturalWind uint32 + StartPlayers int GamePhase GamePhase ShotSync *ShotSyncData Holes []RoomHole diff --git a/game/packet/client.go b/game/packet/client.go index 2016569..7e873ed 100755 --- a/game/packet/client.go +++ b/game/packet/client.go @@ -60,6 +60,7 @@ var ClientMessageTable = common.NewMessageTable(map[uint16]ClientMessage{ 0x0032: &ClientSetIdleStatus{}, 0x0033: &ClientException{}, 0x0034: &ClientFirstShotReady{}, + 0x0037: &ClientLastPlayerLeaveGame{}, 0x0042: &ClientShotArrow{}, 0x0043: &ClientRequestServerList{}, 0x0048: &ClientLoadProgress{}, @@ -132,17 +133,18 @@ type ClientRoomEdit struct { // ClientRoomCreate is sent by the client when creating a room. type ClientRoomCreate struct { ClientMessage_ - Unknown byte - ShotTimerMS uint32 - GameTimerMS uint32 - MaxUsers uint8 - RoomType byte - NumHoles byte - Course byte - Unknown2 [5]byte - RoomName common.PString - Password common.PString - Unknown3 [4]byte + Unknown byte + ShotTimerMS uint32 + GameTimerMS uint32 + MaxUsers uint8 + RoomType byte + NumHoles byte + Course byte + HoleProgression byte + Unknown2 [4]byte + RoomName common.PString + Password common.PString + Unknown3 [4]byte } // ClientRoomJoin is sent by the client when joining a room. @@ -434,6 +436,14 @@ type ClientFirstShotReady struct { ClientMessage_ } +// ClientLastPlayerLeaveGame is sent when the last player leaves a room. +// We can't rely on it for much; it just tells us the client thinks it shouldn't +// be punished for leaving the room since everyone else has already left. +// In the future it may need to be rejected in some cases. +type ClientLastPlayerLeaveGame struct { + ClientMessage_ +} + // ClientRequestPlayerHistory is an unknown message. type ClientRequestPlayerHistory struct { ClientMessage_ diff --git a/game/packet/server.go b/game/packet/server.go index 3d1bfc1..147ab06 100755 --- a/game/packet/server.go +++ b/game/packet/server.go @@ -46,6 +46,7 @@ var ServerMessageTable = common.NewMessageTable(map[uint16]ServerMessage{ 0x005B: &ServerRoomSetWind{}, 0x005D: &ServerRoomUserTypingAnnounce{}, 0x0060: &ServerRoomShotCometReliefAnnounce{}, + 0x0061: &ServerPlayerQuitGame{}, 0x0063: &ServerRoomActiveUserAnnounce{}, 0x0064: &ServerRoomShotSync{}, 0x0065: &ServerRoomFinishHole{}, @@ -580,6 +581,11 @@ type ServerRoomShotCometReliefAnnounce struct { X, Y, Z float32 } +type ServerPlayerQuitGame struct { + ServerMessage_ + ConnID uint32 +} + type ServerRoomActiveUserAnnounce struct { ServerMessage_ ConnID uint32 @@ -599,7 +605,7 @@ type PlayerGameResult struct { Place uint8 Score int8 Unknown uint8 - Unknown2 uint16 + Exp uint16 Pang uint64 BonusPang uint64 Unknown3 uint64 diff --git a/game/room/event.go b/game/room/event.go index 28ba9bf..9a89f52 100644 --- a/game/room/event.go +++ b/game/room/event.go @@ -77,6 +77,7 @@ type RoomPlayerJoin struct { Entry *gamemodel.RoomPlayerEntry PlayerData pangya.PlayerData Conn *gamepacket.ServerConn + UpdateFunc func() } type RoomPlayerLeave struct { @@ -84,6 +85,13 @@ type RoomPlayerLeave struct { ConnID uint32 } +type RoomPlayerUpdateData struct { + roomEvent + ConnID uint32 + Entry *gamemodel.RoomPlayerEntry + PlayerData pangya.PlayerData +} + type RoomAction struct { roomEvent ConnID uint32 diff --git a/game/room/room.go b/game/room/room.go index 0c76933..a5f0278 100644 --- a/game/room/room.go +++ b/game/room/room.go @@ -50,10 +50,13 @@ type RoomPlayer struct { Entry *gamemodel.RoomPlayerEntry Conn *gamepacket.ServerConn PlayerData pangya.PlayerData + UpdateFunc func() GameReady bool ShotSync *gamemodel.ShotSyncData TurnEnd bool HoleEnd bool + GameEnd bool + StartShot bool Pang uint64 BonusPang uint64 LastTotal int8 @@ -186,6 +189,9 @@ func (r *Room) handleEvent(ctx context.Context, t *actor.Task[RoomEvent], msg ac case RoomPlayerLeave: return rejectOnError(r.handlePlayerLeave(ctx, event)) + case RoomPlayerUpdateData: + return rejectOnError(r.handlePlayerUpdateData(ctx, event)) + case RoomAction: return rejectOnError(r.handleRoomAction(ctx, event)) @@ -291,6 +297,7 @@ func (r *Room) handlePlayerJoin(ctx context.Context, event RoomPlayerJoin) error Entry: event.Entry, Conn: event.Conn, PlayerData: event.PlayerData, + UpdateFunc: event.UpdateFunc, }) if present { return errors.New("already in room") @@ -324,6 +331,16 @@ func (r *Room) handlePlayerLeave(ctx context.Context, event RoomPlayerLeave) err return r.removePlayer(ctx, event.ConnID) } +func (r *Room) handlePlayerUpdateData(ctx context.Context, event RoomPlayerUpdateData) error { + if pair := r.players.GetPair(event.ConnID); pair != nil { + *pair.Value.Entry = *event.Entry + pair.Value.PlayerData = event.PlayerData + // TODO: only need to send delta here + return r.broadcastPlayerList(ctx) + } + return nil +} + func (r *Room) handleRoomAction(ctx context.Context, event RoomAction) error { if event.Action.Rotation != nil { if pair := r.players.GetPair(event.ConnID); pair != nil { @@ -469,15 +486,19 @@ func (r *Room) handleRoomStartGame(ctx context.Context, event RoomStartGame) err Players: make([]gamepacket.GamePlayer, r.players.Len()), }, } + r.state.StartPlayers = r.players.Len() for i, pair := 0, r.players.Oldest(); pair != nil; pair = pair.Next() { // Clear ready status. pair.Value.Entry.StatusFlags &^= gamemodel.RoomStateReady - // Set initial turn order. + // Set initial player state. pair.Value.TurnOrder = i pair.Value.Distance = math.Inf(1) pair.Value.Stroke = 0 pair.Value.LastTotal = 0 + pair.Value.TurnEnd = false + pair.Value.HoleEnd = false + pair.Value.GameEnd = false player := pair.Value gameInit.Full.Players[i] = gamepacket.GamePlayer{ @@ -635,6 +656,7 @@ func (r *Room) handleRoomGameShotSync(ctx context.Context, event RoomGameShotSyn Data: *r.state.ShotSync, }) if pair := r.players.GetPair(r.state.ShotSync.ActiveConnID); pair != nil { + pair.Value.StartShot = true pair.Value.Pang = uint64(r.state.ShotSync.Pang) pair.Value.BonusPang = uint64(r.state.ShotSync.BonusPang) @@ -679,9 +701,21 @@ func (r *Room) endTurn(ctx context.Context) error { r.broadcast(ctx, &gamepacket.ServerRoomShotEnd{ ConnID: r.state.ActiveConnID, }) + if pair := r.players.GetPair(r.state.ActiveConnID); pair != nil { + if pair.Value.GameEnd { + r.broadcast(ctx, &gamepacket.ServerRoomPlayerFinished{}) + } + } for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { pair.Value.TurnEnd = false } + return r.nextTurn(ctx) +} + +func (r *Room) nextTurn(ctx context.Context) error { + if pair := r.players.GetPair(r.state.ActiveConnID); pair != nil { + pair.Value.StartShot = false + } nextPlayer := r.getNextPlayer() if nextPlayer == nil { return r.endHole(ctx) @@ -724,15 +758,15 @@ func (r *Room) getNextPlayer() *RoomPlayer { } func (r *Room) endHole(ctx context.Context) error { - r.state.CurrentHole++ - if r.state.CurrentHole > r.state.NumHoles { - return r.endGame(ctx) - } else { + if r.haveNextHole() { + r.advanceHole() r.broadcast(ctx, &gamepacket.ServerRoomFinishHole{}) for pair := r.players.Oldest(); pair != nil; pair = pair.Next() { pair.Value.HoleEnd = false } r.setupNextTurnOrder() + } else { + return r.endGame(ctx) } r.stateUpdated(ctx) return nil @@ -760,17 +794,23 @@ func (r *Room) setupNextTurnOrder() { } func (r *Room) endGame(ctx context.Context) error { + if r.players.Len() == 0 { + return nil + } results := &gamepacket.ServerRoomFinishGame{ NumPlayers: uint8(r.players.Len()), Standings: make([]gamepacket.PlayerGameResult, r.players.Len()), } for i, pair := 0, r.players.Oldest(); pair != nil; pair = pair.Next() { + clearBonus := r.lobby.configProvider.GetCourseBonus(r.state.Course, r.state.StartPlayers, int(r.state.NumHoles)) + exp := int(clearBonus / 2) // TODO: it should be based on course difficulty I believe. bonusPang := pair.Value.BonusPang - bonusPang += r.lobby.configProvider.GetCourseBonus(r.state.Course, r.players.Len(), int(r.state.NumHoles)) + bonusPang += clearBonus results.Standings[i].ConnID = pair.Value.Entry.ConnID results.Standings[i].Pang = pair.Value.Pang results.Standings[i].Score = int8(pair.Value.Score) results.Standings[i].BonusPang = bonusPang + results.Standings[i].Exp = uint16(exp) totalPang := bonusPang + pair.Value.Pang @@ -779,10 +819,18 @@ func (r *Room) endGame(ctx context.Context) error { log.WithError(err).Error("failed giving game-ending pang") } + _, _, err = r.accounts.AddExp(ctx, int64(pair.Value.Entry.PlayerID), exp) + if err != nil { + log.WithError(err).Error("failed giving game-ending exp") + } + if err := pair.Value.Conn.SendMessage(ctx, &gamepacket.ServerPangBalanceData{PangsRemaining: uint64(newPang)}); err != nil { log.WithError(err).Error("failed informing player of game-ending pang") } + // Tell conn to update player so that it sees new EXP/etc. + pair.Value.UpdateFunc() + pair.Value.Score = 0 pair.Value.Pang = 0 pair.Value.BonusPang = 0 @@ -907,6 +955,12 @@ func (r *Room) removePlayer(ctx context.Context, connID uint32) error { }, }) r.players.Delete(connID) + if r.state.GamePhase == gamemodel.InGame { + r.broadcast(ctx, &gamepacket.ServerPlayerQuitGame{ConnID: connID}) + if r.state.ActiveConnID == connID { + r.nextTurn(ctx) + } + } r.lobby.Send(ctx, LobbyPlayerUpdateRoom{ ConnID: connID, RoomNumber: -1, @@ -943,17 +997,30 @@ func (r *Room) roomStatus() *gamepacket.ServerRoomStatus { RoomType: r.state.RoomType, Course: r.state.Course, NumHoles: r.state.NumHoles, - HoleProgression: 1, - NaturalWind: 0, + HoleProgression: r.state.HoleProgression, + NaturalWind: r.state.NaturalWind, MaxUsers: r.state.MaxUsers, ShotTimerMS: r.state.ShotTimerMS, GameTimerMS: r.state.GameTimerMS, - Flags: 0, + Flags: 0, // TODO Owner: true, RoomName: common.ToPString(r.state.RoomName), } } func (r *Room) currentHole() *gamemodel.RoomHole { + // Note: CurrentHole is 1-based. return &r.state.Holes[r.state.CurrentHole-1] } + +func (r *Room) haveNextHole() bool { + // Note: CurrentHole is 1-based. + return r.state.CurrentHole < r.state.NumHoles +} + +func (r *Room) advanceHole() { + // Note: CurrentHole is 1-based. + if r.state.CurrentHole < r.state.NumHoles { + r.state.CurrentHole++ + } +} diff --git a/game/server/conn.go b/game/server/conn.go index 0c0db01..31ab453 100755 --- a/game/server/conn.go +++ b/game/server/conn.go @@ -38,10 +38,11 @@ type Conn struct { *gamepacket.ServerConn s *Server - connID uint32 - session dbmodels.Session - player dbmodels.GetPlayerRow - characters []pangya.PlayerCharacterData + connID uint32 + session dbmodels.Session + player dbmodels.GetPlayerRow + characters []pangya.PlayerCharacterData + updatePlayer chan struct{} currentCharacter *pangya.PlayerCharacterData @@ -135,6 +136,27 @@ func (c *Conn) Handle(ctx context.Context) error { return fmt.Errorf("reading next message: %w", err) } + select { + case <-c.updatePlayer: + c.player, err = c.s.accountsService.GetPlayer(ctx, c.player.PlayerID) + if err != nil { + return fmt.Errorf("updating player data: %w", err) + } + if c.currentLobby != nil { + c.currentLobby.Send(ctx, room.LobbyPlayerUpdate{ + Entry: c.getLobbyPlayer(), + }) + } + if c.currentRoom != nil { + c.currentRoom.Send(ctx, room.RoomPlayerUpdateData{ + ConnID: c.connID, + Entry: c.getRoomPlayer(), + PlayerData: c.getPlayerData(), + }) + } + default: + } + switch t := msg.(type) { case *gamepacket.ClientException: log.WithField("exception", t.Message).Debug("Client exception") @@ -172,15 +194,16 @@ func (c *Conn) Handle(ctx context.Context) error { break } newRoom, err := c.currentLobby.NewRoom(context.Background(), gamemodel.RoomState{ - ShotTimerMS: t.ShotTimerMS, - GameTimerMS: t.GameTimerMS, - MaxUsers: t.MaxUsers, - RoomType: t.RoomType, - NumHoles: t.NumHoles, - Course: t.Course, - RoomName: t.RoomName.Value, - Password: t.Password.Value, - // TODO: natural wind, hole progression, more? + ShotTimerMS: t.ShotTimerMS, + GameTimerMS: t.GameTimerMS, + MaxUsers: t.MaxUsers, + RoomType: t.RoomType, + NumHoles: t.NumHoles, + Course: t.Course, + RoomName: t.RoomName.Value, + Password: t.Password.Value, + HoleProgression: t.HoleProgression, + // TODO: natural wind, more? }) if err != nil { // TODO: handle error @@ -191,6 +214,12 @@ func (c *Conn) Handle(ctx context.Context) error { Entry: c.getRoomPlayer(), Conn: c.ServerConn, PlayerData: c.getPlayerData(), + UpdateFunc: func() { + select { + case c.updatePlayer <- struct{}{}: + default: + } + }, }) case *gamepacket.ClientAssistModeToggle: c.SendMessage(ctx, &gamepacket.ServerAssistModeToggled{}) @@ -400,6 +429,11 @@ func (c *Conn) Handle(ctx context.Context) error { // TODO: handle error return err } + case *gamepacket.ClientLastPlayerLeaveGame: + if err := c.leaveRoom(ctx); err != nil { + // TODO: handle error + return err + } case *gamepacket.ClientRoomKick: if c.currentRoom == nil { break @@ -731,6 +765,15 @@ func (c *Conn) Handle(ctx context.Context) error { return err } } + + // Update the player data in the room, as well. + if c.currentRoom != nil { + c.currentRoom.Send(ctx, room.RoomPlayerUpdateData{ + ConnID: c.connID, + Entry: c.getRoomPlayer(), + PlayerData: c.getPlayerData(), + }) + } case *gamepacket.Client00FE: // TODO log.Debug("TODO: 00FE") diff --git a/game/server/playerdata.go b/game/server/playerdata.go index c2e3a9c..8a222bc 100644 --- a/game/server/playerdata.go +++ b/game/server/playerdata.go @@ -31,8 +31,9 @@ func (c *Conn) getPlayerInfo() pangya.PlayerInfo { func (c *Conn) getPlayerStats() pangya.PlayerStats { return pangya.PlayerStats{ - Pang: uint64(c.player.Pang), - Rank: byte(c.player.Rank), + Pang: uint64(c.player.Pang), + Rank: byte(c.player.Rank), + TotalXP: uint32(c.player.Exp), // TODO } } diff --git a/game/server/server.go b/game/server/server.go index b0ec1dd..6a1c36b 100755 --- a/game/server/server.go +++ b/game/server/server.go @@ -90,7 +90,8 @@ func (s *Server) Listen(ctx context.Context, addr string) error { gamepacket.ClientMessageTable, gamepacket.ServerMessageTable, ), - s: s, + s: s, + updatePlayer: make(chan struct{}, 1), } return conn.Handle(ctx) }) diff --git a/gen/dbmodels/models.go b/gen/dbmodels/models.go index c74707d..fe9540e 100644 --- a/gen/dbmodels/models.go +++ b/gen/dbmodels/models.go @@ -109,6 +109,7 @@ type Player struct { Poster0ID sql.NullInt64 Poster1ID sql.NullInt64 CharacterID sql.NullInt64 + Exp int64 } type Session struct { diff --git a/gen/dbmodels/player.sql.go b/gen/dbmodels/player.sql.go index cbf681d..233da19 100644 --- a/gen/dbmodels/player.sql.go +++ b/gen/dbmodels/player.sql.go @@ -19,7 +19,7 @@ INSERT INTO player ( ) VALUES ( ?, ?, ?, ? ) -RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id +RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp ` type CreatePlayerParams struct { @@ -68,13 +68,14 @@ func (q *Queries) CreatePlayer(ctx context.Context, arg CreatePlayerParams) (Pla &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } const getPlayer = `-- name: GetPlayer :one SELECT - player.player_id, player.username, player.nickname, player.password_hash, player.pang, player.points, player.rank, player.ball_type_id, player.mascot_type_id, player.slot0_type_id, player.slot1_type_id, player.slot2_type_id, player.slot3_type_id, player.slot4_type_id, player.slot5_type_id, player.slot6_type_id, player.slot7_type_id, player.slot8_type_id, player.slot9_type_id, player.caddie_id, player.club_id, player.background_id, player.frame_id, player.sticker_id, player.slot_id, player.cut_in_id, player.title_id, player.poster0_id, player.poster1_id, player.character_id, + player.player_id, player.username, player.nickname, player.password_hash, player.pang, player.points, player.rank, player.ball_type_id, player.mascot_type_id, player.slot0_type_id, player.slot1_type_id, player.slot2_type_id, player.slot3_type_id, player.slot4_type_id, player.slot5_type_id, player.slot6_type_id, player.slot7_type_id, player.slot8_type_id, player.slot9_type_id, player.caddie_id, player.club_id, player.background_id, player.frame_id, player.sticker_id, player.slot_id, player.cut_in_id, player.title_id, player.poster0_id, player.poster1_id, player.character_id, player.exp, character.character_id, character.player_id, character.item_id, character.hair_color, character.shirt, character.mastery, character.part00_item_id, character.part01_item_id, character.part02_item_id, character.part03_item_id, character.part04_item_id, character.part05_item_id, character.part06_item_id, character.part07_item_id, character.part08_item_id, character.part09_item_id, character.part10_item_id, character.part11_item_id, character.part12_item_id, character.part13_item_id, character.part14_item_id, character.part15_item_id, character.part16_item_id, character.part17_item_id, character.part18_item_id, character.part19_item_id, character.part20_item_id, character.part21_item_id, character.part22_item_id, character.part23_item_id, character.part00_item_type_id, character.part01_item_type_id, character.part02_item_type_id, character.part03_item_type_id, character.part04_item_type_id, character.part05_item_type_id, character.part06_item_type_id, character.part07_item_type_id, character.part08_item_type_id, character.part09_item_type_id, character.part10_item_type_id, character.part11_item_type_id, character.part12_item_type_id, character.part13_item_type_id, character.part14_item_type_id, character.part15_item_type_id, character.part16_item_type_id, character.part17_item_type_id, character.part18_item_type_id, character.part19_item_type_id, character.part20_item_type_id, character.part21_item_type_id, character.part22_item_type_id, character.part23_item_type_id, character.aux_part0_id, character.aux_part1_id, character.aux_part2_id, character.aux_part3_id, character.aux_part4_id, character.cut_in_id, inventory_character.item_type_id AS character_type_id_, inventory_caddie.item_type_id AS caddie_type_id_, @@ -135,6 +136,7 @@ type GetPlayerRow struct { Poster0ID sql.NullInt64 Poster1ID sql.NullInt64 CharacterID sql.NullInt64 + Exp int64 CharacterID_2 int64 PlayerID_2 int64 ItemID int64 @@ -242,6 +244,7 @@ func (q *Queries) GetPlayer(ctx context.Context, playerID int64) (GetPlayerRow, &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, &i.CharacterID_2, &i.PlayerID_2, &i.ItemID, @@ -318,7 +321,7 @@ func (q *Queries) GetPlayer(ctx context.Context, playerID int64) (GetPlayerRow, } const getPlayerByUsername = `-- name: GetPlayerByUsername :one -SELECT player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id FROM player +SELECT player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp FROM player WHERE username = ? LIMIT 1 ` @@ -357,6 +360,7 @@ func (q *Queries) GetPlayerByUsername(ctx context.Context, username string) (Pla &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } @@ -424,8 +428,24 @@ func (q *Queries) GetPlayerCurrency(ctx context.Context, playerID int64) (GetPla return i, err } +const getPlayerRank = `-- name: GetPlayerRank :one +SELECT rank, exp FROM player WHERE player_id = ? +` + +type GetPlayerRankRow struct { + Rank int64 + Exp int64 +} + +func (q *Queries) GetPlayerRank(ctx context.Context, playerID int64) (GetPlayerRankRow, error) { + row := q.db.QueryRowContext(ctx, getPlayerRank, playerID) + var i GetPlayerRankRow + err := row.Scan(&i.Rank, &i.Exp) + return i, err +} + const setPlayerCaddie = `-- name: SetPlayerCaddie :one -UPDATE player SET caddie_id = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id +UPDATE player SET caddie_id = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp ` type SetPlayerCaddieParams struct { @@ -467,12 +487,13 @@ func (q *Queries) SetPlayerCaddie(ctx context.Context, arg SetPlayerCaddieParams &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } const setPlayerCharacter = `-- name: SetPlayerCharacter :one -UPDATE player SET character_id = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id +UPDATE player SET character_id = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp ` type SetPlayerCharacterParams struct { @@ -514,12 +535,13 @@ func (q *Queries) SetPlayerCharacter(ctx context.Context, arg SetPlayerCharacter &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } const setPlayerClubSet = `-- name: SetPlayerClubSet :one -UPDATE player SET club_id = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id +UPDATE player SET club_id = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp ` type SetPlayerClubSetParams struct { @@ -561,12 +583,13 @@ func (q *Queries) SetPlayerClubSet(ctx context.Context, arg SetPlayerClubSetPara &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } const setPlayerComet = `-- name: SetPlayerComet :one -UPDATE player SET ball_type_id = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id +UPDATE player SET ball_type_id = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp ` type SetPlayerCometParams struct { @@ -608,6 +631,7 @@ func (q *Queries) SetPlayerComet(ctx context.Context, arg SetPlayerCometParams) &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } @@ -626,7 +650,7 @@ SET slot8_type_id = ?, slot9_type_id = ? WHERE player_id = ? -RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id +RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp ` type SetPlayerConsumablesParams struct { @@ -689,6 +713,7 @@ func (q *Queries) SetPlayerConsumables(ctx context.Context, arg SetPlayerConsuma &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } @@ -725,7 +750,7 @@ SET cut_in_id = ?, title_id = ? WHERE player_id = ? -RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id +RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp ` type SetPlayerDecorationParams struct { @@ -780,12 +805,13 @@ func (q *Queries) SetPlayerDecoration(ctx context.Context, arg SetPlayerDecorati &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } const setPlayerNickname = `-- name: SetPlayerNickname :one -UPDATE player SET nickname = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id +UPDATE player SET nickname = ? WHERE player_id = ? RETURNING player_id, username, nickname, password_hash, pang, points, rank, ball_type_id, mascot_type_id, slot0_type_id, slot1_type_id, slot2_type_id, slot3_type_id, slot4_type_id, slot5_type_id, slot6_type_id, slot7_type_id, slot8_type_id, slot9_type_id, caddie_id, club_id, background_id, frame_id, sticker_id, slot_id, cut_in_id, title_id, poster0_id, poster1_id, character_id, exp ` type SetPlayerNicknameParams struct { @@ -827,6 +853,29 @@ func (q *Queries) SetPlayerNickname(ctx context.Context, arg SetPlayerNicknamePa &i.Poster0ID, &i.Poster1ID, &i.CharacterID, + &i.Exp, ) return i, err } + +const setPlayerRank = `-- name: SetPlayerRank :one +UPDATE player SET rank = ?, exp = ? WHERE player_id = ? RETURNING rank, exp +` + +type SetPlayerRankParams struct { + Rank int64 + Exp int64 + PlayerID int64 +} + +type SetPlayerRankRow struct { + Rank int64 + Exp int64 +} + +func (q *Queries) SetPlayerRank(ctx context.Context, arg SetPlayerRankParams) (SetPlayerRankRow, error) { + row := q.db.QueryRowContext(ctx, setPlayerRank, arg.Rank, arg.Exp, arg.PlayerID) + var i SetPlayerRankRow + err := row.Scan(&i.Rank, &i.Exp) + return i, err +} diff --git a/migrations/0002_exp.sql b/migrations/0002_exp.sql new file mode 100644 index 0000000..411f69b --- /dev/null +++ b/migrations/0002_exp.sql @@ -0,0 +1,5 @@ +-- +goose Up +ALTER TABLE player ADD COLUMN exp INTEGER NOT NULL DEFAULT 0; + +-- +goose Down +ALTER TABLE player DROP COLUMN exp; diff --git a/pangya/rank.go b/pangya/rank.go index 54fa1dd..edab118 100755 --- a/pangya/rank.go +++ b/pangya/rank.go @@ -94,3 +94,97 @@ const ( InfinityLegendB Rank = 0x45 InfinityLegendA Rank = 0x46 ) + +// RankExperience contains the experience points needed to level up from each +// rank. +// TODO: need to ensure this does not differ by version/etc. +var RankExperience = map[Rank]int{ + RookieF: 30, + RookieE: 40, + RookieD: 50, + RookieC: 60, + RookieB: 70, + RookieA: 140, + BeginnerE: 105, + BeginnerD: 125, + BeginnerC: 145, + BeginnerB: 165, + BeginnerA: 330, + JuniorE: 248, + JuniorD: 278, + JuniorC: 308, + JuniorB: 338, + JuniorA: 675, + SeniorE: 506, + SeniorD: 546, + SeniorC: 586, + SeniorB: 626, + SeniorA: 1253, + AmateurE: 1002, + AmateurD: 1052, + AmateurC: 1102, + AmateurB: 1152, + AmateurA: 2304, + SemiProE: 1843, + SemiProD: 1903, + SemiProC: 1963, + SemiProB: 2023, + SemiProA: 4046, + ProE: 3237, + ProD: 3307, + ProC: 3377, + ProB: 3447, + ProA: 6894, + NationalProE: 5515, + NationalProD: 5595, + NationalProC: 5675, + NationalProB: 5755, + NationalProA: 11511, + WorldProE: 8058, + WorldProD: 8148, + WorldProC: 8238, + WorldProB: 8328, + WorldProA: 16655, + MasterE: 8328, + MasterD: 8428, + MasterC: 8528, + MasterB: 8628, + MasterA: 17255, + TopMasterE: 9490, + TopMasterD: 9690, + TopMasterC: 9890, + TopMasterB: 10090, + TopMasterA: 20181, + WorldMasterE: 20181, + WorldMasterD: 20481, + WorldMasterC: 20781, + WorldMasterB: 21081, + WorldMasterA: 42161, + LegendE: 37945, + LegendD: 68301, + LegendC: 122942, + LegendB: 221296, + LegendA: 442592, + InfinityLegendE: 663887, + InfinityLegendD: 995831, + InfinityLegendC: 1493747, + InfinityLegendB: 2240620, + InfinityLegendA: -1, +} + +func AddExperience(rank Rank, current, amount int) (newRank Rank, newExp int) { + sum := current + amount + for { + rankExp := RankExperience[rank] + if rankExp == -1 { + // Max rank; no EXP. + return rank, 0 + } + if sum >= rankExp { + sum -= rankExp + rank++ + } else { + return rank, sum + } + } +} diff --git a/queries/player.sql b/queries/player.sql index 6ea5311..bff6db5 100644 --- a/queries/player.sql +++ b/queries/player.sql @@ -107,4 +107,10 @@ RETURNING *; SELECT pang, points FROM player WHERE player_id = ?; -- name: SetPlayerCurrency :one -UPDATE player SET pang = ?, points = ? WHERE player_id = ? RETURNING pang, points; \ No newline at end of file +UPDATE player SET pang = ?, points = ? WHERE player_id = ? RETURNING pang, points; + +-- name: GetPlayerRank :one +SELECT rank, exp FROM player WHERE player_id = ?; + +-- name: SetPlayerRank :one +UPDATE player SET rank = ?, exp = ? WHERE player_id = ? RETURNING rank, exp;