From 099155c186add15245f7f5a345ca823fb44ad765 Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Sat, 27 Apr 2024 22:32:05 +0300 Subject: [PATCH] New drawing system, start of inventory menu --- game/game.go | 16 +- game/game_context.go | 55 +++++++ game/model/dungeon.go | 1 + game/model/empty_dungeon_level.go | 47 ++++++ game/model/entity.go | 26 +++- game/model/flat_ground_dungeon_level.go | 75 +++++++++ game/model/map.go | 12 ++ game/model/multilevel_map.go | 61 ++++++++ game/model/player.go | 53 ++----- game/model/tile.go | 86 +++++++++++ game/state/{gameState.go => game_state.go} | 5 +- game/state/inventory_screen_state.go | 51 +++++++ .../{mainMenuState.go => main_menu_state.go} | 11 +- ...{pauseGameState.go => pause_game_state.go} | 23 +-- game/state/playingState.go | 56 ------- game/state/playing_state.go | 144 ++++++++++++++++++ game/state/{quitState.go => quit_state.go} | 7 +- main.go | 51 +------ render/arbitrary_drawable.go | 28 ++++ render/grid.go | 22 +-- render/raw.go | 35 ++++- render/rectangle.go | 16 +- render/{context.go => render_context.go} | 120 ++++++++------- render/text.go | 4 +- render/viewport.go | 96 ++++++++++++ ui/{borderButton.go => border_button.go} | 2 +- ui/container.go | 4 +- ui/label.go | 10 +- ui/{simpleButton.go => simple_button.go} | 16 +- ui/ui.go | 9 +- ui/window.go | 8 +- util/util.go | 18 ++- 32 files changed, 890 insertions(+), 278 deletions(-) create mode 100644 game/game_context.go create mode 100644 game/model/dungeon.go create mode 100644 game/model/empty_dungeon_level.go create mode 100644 game/model/flat_ground_dungeon_level.go create mode 100644 game/model/map.go create mode 100644 game/model/multilevel_map.go create mode 100644 game/model/tile.go rename game/state/{gameState.go => game_state.go} (76%) create mode 100644 game/state/inventory_screen_state.go rename game/state/{mainMenuState.go => main_menu_state.go} (92%) rename game/state/{pauseGameState.go => pause_game_state.go} (76%) delete mode 100644 game/state/playingState.go create mode 100644 game/state/playing_state.go rename game/state/{quitState.go => quit_state.go} (62%) create mode 100644 render/arbitrary_drawable.go rename render/{context.go => render_context.go} (51%) create mode 100644 render/viewport.go rename ui/{borderButton.go => border_button.go} (95%) rename ui/{simpleButton.go => simple_button.go} (73%) diff --git a/game/game.go b/game/game.go index a4522df..44a9a05 100644 --- a/game/game.go +++ b/game/game.go @@ -2,13 +2,15 @@ package game import ( "mvvasilev/last_light/game/state" + "mvvasilev/last_light/render" "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" ) type Game struct { state state.GameState + + quitGame bool } func CreateGame() *Game { @@ -20,10 +22,18 @@ func CreateGame() *Game { } func (g *Game) Input(ev *tcell.EventKey) { + if ev.Key() == tcell.KeyCtrlC { + g.quitGame = true + } + g.state.OnInput(ev) } func (g *Game) Tick(dt int64) bool { + if g.quitGame { + return false + } + s := g.state.OnTick(dt) switch s.(type) { @@ -36,6 +46,6 @@ func (g *Game) Tick(dt int64) bool { return true } -func (g *Game) Draw(v views.View) { - g.state.OnDraw(v) +func (g *Game) CollectDrawables() []render.Drawable { + return g.state.CollectDrawables() } diff --git a/game/game_context.go b/game/game_context.go new file mode 100644 index 0000000..f862815 --- /dev/null +++ b/game/game_context.go @@ -0,0 +1,55 @@ +package game + +import ( + "log" + "mvvasilev/last_light/render" + "os" + "time" +) + +const TICK_RATE int64 = 50 // tick every 50ms ( 20 ticks per second ) + +type GameContext struct { + renderContext *render.RenderContext + + game *Game +} + +func CreateGameContext() *GameContext { + gc := new(GameContext) + + rc, err := render.CreateRenderContext() + + if err != nil { + log.Fatalf("%~v", err) + } + + gc.renderContext = rc + gc.game = CreateGame() + + return gc +} + +func (gc *GameContext) Run() { + lastTick := time.Now() + + for { + deltaTime := 1 + time.Since(lastTick).Microseconds() + lastTick = time.Now() + + for _, e := range gc.renderContext.CollectInputEvents() { + gc.game.Input(e) + } + + stop := !gc.game.Tick(deltaTime) + + if stop { + gc.renderContext.Stop() + os.Exit(0) + break + } + + drawables := gc.game.CollectDrawables() + gc.renderContext.Draw(deltaTime, drawables) + } +} diff --git a/game/model/dungeon.go b/game/model/dungeon.go new file mode 100644 index 0000000..8b53790 --- /dev/null +++ b/game/model/dungeon.go @@ -0,0 +1 @@ +package model diff --git a/game/model/empty_dungeon_level.go b/game/model/empty_dungeon_level.go new file mode 100644 index 0000000..0f0f6c2 --- /dev/null +++ b/game/model/empty_dungeon_level.go @@ -0,0 +1,47 @@ +package model + +import "mvvasilev/last_light/util" + +type EmptyDungeonLevel struct { + tiles [][]Tile +} + +func CreateEmptyDungeonLevel(width, height int) *EmptyDungeonLevel { + m := new(EmptyDungeonLevel) + + m.tiles = make([][]Tile, height) + + for h := range height { + m.tiles[h] = make([]Tile, width) + } + + return m +} + +func (edl *EmptyDungeonLevel) Size() util.Size { + return util.SizeOf(len(edl.tiles[0]), len(edl.tiles)) +} + +func (edl *EmptyDungeonLevel) SetTileAt(x int, y int, t Tile) { + if len(edl.tiles) <= y || len(edl.tiles[0]) <= x { + return + } + + edl.tiles[y][x] = t +} + +func (edl *EmptyDungeonLevel) TileAt(x int, y int) Tile { + if y < 0 || y >= len(edl.tiles) { + return nil + } + + if x < 0 || x >= len(edl.tiles[y]) { + return nil + } + + return edl.tiles[y][x] +} + +func (edl *EmptyDungeonLevel) Tick() { + +} diff --git a/game/model/entity.go b/game/model/entity.go index c6da679..94b8dba 100644 --- a/game/model/entity.go +++ b/game/model/entity.go @@ -4,22 +4,36 @@ import ( "mvvasilev/last_light/util" "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" "github.com/google/uuid" ) type Direction int const ( - Up Direction = iota - Down - Left - Right + DirectionNone Direction = iota + DirectionUp + DirectionDown + DirectionLeft + DirectionRight ) +func MovementDirectionOffset(dir Direction) (int, int) { + switch dir { + case DirectionUp: + return 0, -1 + case DirectionDown: + return 0, 1 + case DirectionLeft: + return -1, 0 + case DirectionRight: + return 1, 0 + } + + return 0, 0 +} + type Entity interface { UniqueId() uuid.UUID - Draw(v views.View) Input(e *tcell.EventKey) Tick(dt int64) } diff --git a/game/model/flat_ground_dungeon_level.go b/game/model/flat_ground_dungeon_level.go new file mode 100644 index 0000000..bf85da9 --- /dev/null +++ b/game/model/flat_ground_dungeon_level.go @@ -0,0 +1,75 @@ +package model + +import ( + "math/rand" + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" +) + +type FlatGroundDungeonLevel struct { + tiles [][]Tile +} + +func CreateFlatGroundDungeonLevel(width, height int) *FlatGroundDungeonLevel { + level := new(FlatGroundDungeonLevel) + + level.tiles = make([][]Tile, height) + + for h := range height { + level.tiles[h] = make([]Tile, width) + + for w := range width { + if w == 0 || h == 0 || w >= width-1 || h >= height-1 { + level.tiles[h][w] = CreateStaticTile(w, h, Rock()) + continue + } + + level.tiles[h][w] = genRandomGroundTile(w, h) + } + } + + return level +} + +func genRandomGroundTile(width, height int) Tile { + switch rand.Intn(2) { + case 0: + return CreateStaticTile(width, height, Ground()) + case 1: + return CreateStaticTile(width, height, Grass()) + default: + return CreateStaticTile(width, height, Ground()) + } +} + +func (edl *FlatGroundDungeonLevel) Size() util.Size { + return util.SizeOfInt(len(edl.tiles[0]), len(edl.tiles)) +} + +func (edl *FlatGroundDungeonLevel) SetTileAt(x int, y int, t Tile) { + if len(edl.tiles) <= y || len(edl.tiles[0]) <= x { + return + } + + edl.tiles[y][x] = t +} + +func (edl *FlatGroundDungeonLevel) TileAt(x int, y int) Tile { + if y < 0 || y >= len(edl.tiles) { + return nil + } + + if x < 0 || x >= len(edl.tiles[y]) { + return nil + } + + return edl.tiles[y][x] +} + +func (edl *FlatGroundDungeonLevel) Input(e *tcell.EventKey) { + +} + +func (edl *FlatGroundDungeonLevel) Tick() { +} diff --git a/game/model/map.go b/game/model/map.go new file mode 100644 index 0000000..39b1674 --- /dev/null +++ b/game/model/map.go @@ -0,0 +1,12 @@ +package model + +import ( + "mvvasilev/last_light/util" +) + +type Map interface { + Size() util.Size + SetTileAt(x, y int, t Tile) + TileAt(x, y int) Tile + Tick() +} diff --git a/game/model/multilevel_map.go b/game/model/multilevel_map.go new file mode 100644 index 0000000..ab9777d --- /dev/null +++ b/game/model/multilevel_map.go @@ -0,0 +1,61 @@ +package model + +import "mvvasilev/last_light/util" + +type MultilevelMap struct { + layers []Map +} + +func CreateMultilevelMap(maps ...Map) *MultilevelMap { + m := new(MultilevelMap) + + m.layers = maps + + return m +} + +func (mm *MultilevelMap) Size() util.Size { + if len(mm.layers) == 0 { + return util.SizeOf(0, 0) + } + + return mm.layers[0].Size() +} + +func (mm *MultilevelMap) SetTileAt(x, y int, t Tile) { + mm.layers[0].SetTileAt(x, y, t) +} + +func (mm *MultilevelMap) UnsetTileAtHeight(x, y, height int) { + if len(mm.layers) < height { + return + } + + mm.layers[height].SetTileAt(x, y, nil) +} + +func (mm *MultilevelMap) SetTileAtHeight(x, y, height int, t Tile) { + if len(mm.layers) < height { + return + } + + mm.layers[height].SetTileAt(x, y, t) +} + +func (mm *MultilevelMap) TileAt(x int, y int) Tile { + for i := len(mm.layers) - 1; i >= 0; i-- { + tile := mm.layers[i].TileAt(x, y) + + if tile != nil { + return tile + } + } + + return nil +} + +func (mm *MultilevelMap) Tick() { + for _, l := range mm.layers { + l.Tick() + } +} diff --git a/game/model/player.go b/game/model/player.go index ba60c0e..2709020 100644 --- a/game/model/player.go +++ b/game/model/player.go @@ -4,17 +4,15 @@ import ( "mvvasilev/last_light/util" "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" "github.com/google/uuid" ) type Player struct { id uuid.UUID position util.Position - style tcell.Style } -func CreatePlayer(x, y uint16, style tcell.Style) *Player { +func CreatePlayer(x, y int) *Player { p := new(Player) p.id = uuid.New() @@ -27,48 +25,23 @@ func (p *Player) UniqueId() uuid.UUID { return p.id } -func (p *Player) Move(dir Direction) { - x, y := p.position.XYUint16() - - switch dir { - case Up: - p.position = util.PositionAt(x, y-1) - case Down: - p.position = util.PositionAt(x, y+1) - case Left: - p.position = util.PositionAt(x-1, y) - case Right: - p.position = util.PositionAt(x+1, y) - } +func (p *Player) Position() util.Position { + return p.position } -func (p *Player) Draw(v views.View) { - x, y := p.position.XY() - v.SetContent(x, y, '@', nil, p.style) +func (p *Player) Move(dir Direction) { + p.position = p.Position().WithOffset(MovementDirectionOffset(dir)) +} + +func (p *Player) Presentation() rune { + return '@' +} + +func (p *Player) Passable() bool { + return false } func (p *Player) Input(e *tcell.EventKey) { - switch e.Key() { - case tcell.KeyUp: - p.Move(Up) - case tcell.KeyDown: - p.Move(Down) - case tcell.KeyLeft: - p.Move(Left) - case tcell.KeyRight: - p.Move(Right) - case tcell.KeyRune: - switch e.Rune() { - case 'w': - p.Move(Up) - case 'a': - p.Move(Left) - case 's': - p.Move(Down) - case 'd': - p.Move(Right) - } - } } func (p *Player) Tick(dt int64) { diff --git a/game/model/tile.go b/game/model/tile.go new file mode 100644 index 0000000..763c281 --- /dev/null +++ b/game/model/tile.go @@ -0,0 +1,86 @@ +package model + +import "mvvasilev/last_light/util" + +type Material uint + +const ( + MaterialGround Material = iota + MaterialRock + MaterialGrass + MaterialVoid +) + +type TileType struct { + Material Material + Passable bool + Presentation rune +} + +func Ground() TileType { + return TileType{ + Material: MaterialGround, + Passable: true, + Presentation: '.', + } +} + +func Rock() TileType { + return TileType{ + Material: MaterialRock, + Passable: false, + Presentation: '█', + } +} + +func Grass() TileType { + return TileType{ + Material: MaterialGrass, + Passable: true, + Presentation: ',', + } +} + +func Void() TileType { + return TileType{ + Material: MaterialVoid, + Passable: false, + Presentation: ' ', + } +} + +type Tile interface { + Position() util.Position + Presentation() rune + Passable() bool +} + +type StaticTile struct { + position util.Position + t TileType +} + +func CreateStaticTile(x, y int, t TileType) Tile { + st := new(StaticTile) + + st.position = util.PositionAt(x, y) + st.t = t + + return st +} + +func (st *StaticTile) Position() util.Position { + return st.position +} + +func (st *StaticTile) Presentation() rune { + return st.t.Presentation +} + +func (st *StaticTile) Passable() bool { + return st.t.Passable +} + +func (st *StaticTile) Type() TileType { + return st.t +} diff --git a/game/state/gameState.go b/game/state/game_state.go similarity index 76% rename from game/state/gameState.go rename to game/state/game_state.go index 2d606b2..2a6f683 100644 --- a/game/state/gameState.go +++ b/game/state/game_state.go @@ -1,14 +1,15 @@ package state import ( + "mvvasilev/last_light/render" + "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" ) type GameState interface { OnInput(e *tcell.EventKey) OnTick(dt int64) GameState - OnDraw(c views.View) + CollectDrawables() []render.Drawable } type PausableState interface { diff --git a/game/state/inventory_screen_state.go b/game/state/inventory_screen_state.go new file mode 100644 index 0000000..13e5b5a --- /dev/null +++ b/game/state/inventory_screen_state.go @@ -0,0 +1,51 @@ +package state + +import ( + "mvvasilev/last_light/game/model" + "mvvasilev/last_light/render" + "mvvasilev/last_light/ui" + + "github.com/gdamore/tcell/v2" +) + +type InventoryScreenState struct { + prevState GameState + exitMenu bool + + inventoryMenu *ui.UIWindow + + player *model.Player +} + +func CreateInventoryScreenState(player *model.Player, prevState GameState) *InventoryScreenState { + iss := new(InventoryScreenState) + + iss.prevState = prevState + iss.player = player + iss.exitMenu = false + + iss.inventoryMenu = ui.CreateWindow(40, 0, 40, 24, "INVENTORY", tcell.StyleDefault) + + return iss +} + +func (iss *InventoryScreenState) OnInput(e *tcell.EventKey) { + if e.Key() == tcell.KeyEsc || (e.Key() == tcell.KeyRune && e.Rune() == 'i') { + iss.exitMenu = true + } +} + +func (iss *InventoryScreenState) OnTick(dt int64) GameState { + if iss.exitMenu { + return iss.prevState + } + + return iss +} + +func (iss *InventoryScreenState) CollectDrawables() []render.Drawable { + return append( + iss.prevState.CollectDrawables(), + iss.inventoryMenu, + ) +} diff --git a/game/state/mainMenuState.go b/game/state/main_menu_state.go similarity index 92% rename from game/state/mainMenuState.go rename to game/state/main_menu_state.go index fba766b..a78ed34 100644 --- a/game/state/mainMenuState.go +++ b/game/state/main_menu_state.go @@ -6,7 +6,6 @@ import ( "mvvasilev/last_light/util" "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" ) type MainMenuState struct { @@ -78,10 +77,14 @@ func (mms *MainMenuState) OnTick(dt int64) GameState { return mms } -func (mms *MainMenuState) OnDraw(c views.View) { - mms.menuTitle.Draw(c) +func (mms *MainMenuState) CollectDrawables() []render.Drawable { + arr := make([]render.Drawable, 0) + + arr = append(arr, mms.menuTitle) for _, b := range mms.buttons { - b.Draw(c) + arr = append(arr, b) } + + return arr } diff --git a/game/state/pauseGameState.go b/game/state/pause_game_state.go similarity index 76% rename from game/state/pauseGameState.go rename to game/state/pause_game_state.go index 13a3453..81db8d8 100644 --- a/game/state/pauseGameState.go +++ b/game/state/pause_game_state.go @@ -6,7 +6,6 @@ import ( "mvvasilev/last_light/util" "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" ) type PauseGameState struct { @@ -27,13 +26,13 @@ func PauseGame(prevState PausableState) *PauseGameState { highlightStyle := tcell.StyleDefault.Attributes(tcell.AttrBold) - s.pauseMenuWindow = ui.CreateWindow(uint16(render.TERMINAL_SIZE_WIDTH)/2-15, uint16(render.TERMINAL_SIZE_HEIGHT)/2-7, 30, 14, "PAUSED", tcell.StyleDefault) + s.pauseMenuWindow = ui.CreateWindow(int(render.TERMINAL_SIZE_WIDTH)/2-15, int(render.TERMINAL_SIZE_HEIGHT)/2-7, 30, 14, "PAUSED", tcell.StyleDefault) s.buttons = make([]*ui.UISimpleButton, 0) s.buttons = append( s.buttons, ui.CreateSimpleButton( - uint16(s.pauseMenuWindow.Position().X())+3, - uint16(s.pauseMenuWindow.Position().Y())+1, + int(s.pauseMenuWindow.Position().X())+3, + int(s.pauseMenuWindow.Position().Y())+1, "Resume", tcell.StyleDefault, highlightStyle, @@ -45,8 +44,8 @@ func PauseGame(prevState PausableState) *PauseGameState { s.buttons = append( s.buttons, ui.CreateSimpleButton( - uint16(s.pauseMenuWindow.Position().X())+3, - uint16(s.pauseMenuWindow.Position().Y())+3, + int(s.pauseMenuWindow.Position().X())+3, + int(s.pauseMenuWindow.Position().Y())+3, "Exit To Main Menu", tcell.StyleDefault, highlightStyle, @@ -97,12 +96,16 @@ func (pg *PauseGameState) OnTick(dt int64) GameState { return pg } -func (pg *PauseGameState) OnDraw(c views.View) { - pg.prevState.OnDraw(c) +func (pg *PauseGameState) CollectDrawables() []render.Drawable { + arr := make([]render.Drawable, 0) - pg.pauseMenuWindow.Draw(c) + arr = append(arr, pg.prevState.CollectDrawables()...) + + arr = append(arr, pg.pauseMenuWindow) for _, b := range pg.buttons { - b.Draw(c) + arr = append(arr, b) } + + return arr } diff --git a/game/state/playingState.go b/game/state/playingState.go deleted file mode 100644 index 0cd6e3d..0000000 --- a/game/state/playingState.go +++ /dev/null @@ -1,56 +0,0 @@ -package state - -import ( - "mvvasilev/last_light/game/model" - - "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" -) - -type PlayingState struct { - player *model.Player - - pauseGame bool -} - -func BeginPlayingState() *PlayingState { - s := new(PlayingState) - - s.player = model.CreatePlayer(10, 10, tcell.StyleDefault) - - return s -} - -func (ps *PlayingState) Pause() { - ps.pauseGame = true -} - -func (ps *PlayingState) Unpause() { - ps.pauseGame = false -} - -func (ps *PlayingState) SetPaused(paused bool) { - ps.pauseGame = paused -} - -func (ps *PlayingState) OnInput(e *tcell.EventKey) { - ps.player.Input(e) - - if e.Key() == tcell.KeyEsc { - ps.pauseGame = true - } -} - -func (ps *PlayingState) OnTick(dt int64) GameState { - ps.player.Tick(dt) - - if ps.pauseGame { - return PauseGame(ps) - } - - return ps -} - -func (ps *PlayingState) OnDraw(c views.View) { - ps.player.Draw(c) -} diff --git a/game/state/playing_state.go b/game/state/playing_state.go new file mode 100644 index 0000000..6d64251 --- /dev/null +++ b/game/state/playing_state.go @@ -0,0 +1,144 @@ +package state + +import ( + "mvvasilev/last_light/game/model" + "mvvasilev/last_light/render" + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" +) + +type PlayingState struct { + player *model.Player + level *model.MultilevelMap + + viewport *render.Viewport + + movePlayerDirection model.Direction + pauseGame bool + openInventory bool +} + +func BeginPlayingState() *PlayingState { + s := new(PlayingState) + + mapSize := util.SizeOf(128, 128) + + s.player = model.CreatePlayer(40, 12) + + s.level = model.CreateMultilevelMap( + model.CreateFlatGroundDungeonLevel(mapSize.WH()), + model.CreateEmptyDungeonLevel(mapSize.WH()), + ) + + s.level.SetTileAtHeight(40, 12, 1, s.player) + + s.viewport = render.CreateViewport( + util.PositionAt(0, 0), + util.PositionAt(40, 12), + util.SizeOf(80, 24), + tcell.StyleDefault, + ) + + return s +} + +func (ps *PlayingState) Pause() { + ps.pauseGame = true +} + +func (ps *PlayingState) Unpause() { + ps.pauseGame = false +} + +func (ps *PlayingState) SetPaused(paused bool) { + ps.pauseGame = paused +} + +func (ps *PlayingState) MovePlayer() { + if ps.movePlayerDirection == model.DirectionNone { + return + } + + newPlayerPos := ps.player.Position().WithOffset(model.MovementDirectionOffset(ps.movePlayerDirection)) + + tileAtMovePos := ps.level.TileAt(newPlayerPos.XY()) + + if tileAtMovePos.Passable() { + ps.level.SetTileAtHeight(ps.player.Position().X(), ps.player.Position().Y(), 1, nil) + ps.player.Move(ps.movePlayerDirection) + ps.viewport.SetCenter(ps.player.Position()) + ps.level.SetTileAtHeight(ps.player.Position().X(), ps.player.Position().Y(), 1, ps.player) + } + + ps.movePlayerDirection = model.DirectionNone +} + +func (ps *PlayingState) OnInput(e *tcell.EventKey) { + ps.player.Input(e) + + if e.Key() == tcell.KeyEsc { + ps.pauseGame = true + return + } + + if e.Key() == tcell.KeyRune && e.Rune() == 'i' { + ps.openInventory = true + return + } + + switch e.Key() { + case tcell.KeyUp: + ps.movePlayerDirection = model.DirectionUp + case tcell.KeyDown: + ps.movePlayerDirection = model.DirectionDown + case tcell.KeyLeft: + ps.movePlayerDirection = model.DirectionLeft + case tcell.KeyRight: + ps.movePlayerDirection = model.DirectionRight + case tcell.KeyRune: + switch e.Rune() { + case 'w': + ps.movePlayerDirection = model.DirectionUp + case 'a': + ps.movePlayerDirection = model.DirectionLeft + case 's': + ps.movePlayerDirection = model.DirectionDown + case 'd': + ps.movePlayerDirection = model.DirectionRight + } + } +} + +func (ps *PlayingState) OnTick(dt int64) GameState { + ps.player.Tick(dt) + + if ps.pauseGame { + return PauseGame(ps) + } + + if ps.openInventory { + return CreateInventoryScreenState(ps.player, ps) + } + + if ps.movePlayerDirection != model.DirectionNone { + ps.MovePlayer() + } + + return ps +} + +func (ps *PlayingState) CollectDrawables() []render.Drawable { + return render.Multidraw(render.CreateDrawingInstructions(func(v views.View) { + ps.viewport.DrawFromProvider(v, func(x, y int) rune { + tile := ps.level.TileAt(x, y) + + if tile != nil { + return tile.Presentation() + } + + return ' ' + }) + })) +} diff --git a/game/state/quitState.go b/game/state/quit_state.go similarity index 62% rename from game/state/quitState.go rename to game/state/quit_state.go index 37e0d46..ef9ae0a 100644 --- a/game/state/quitState.go +++ b/game/state/quit_state.go @@ -1,8 +1,9 @@ package state import ( + "mvvasilev/last_light/render" + "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" ) type QuitState struct { @@ -16,6 +17,6 @@ func (q *QuitState) OnTick(dt int64) GameState { return q } -func (q *QuitState) OnDraw(c views.View) { - +func (q *QuitState) CollectDrawables() []render.Drawable { + return render.Multidraw(nil) } diff --git a/main.go b/main.go index 2520b47..8ccb7fc 100644 --- a/main.go +++ b/main.go @@ -1,53 +1,8 @@ package main -import ( - "fmt" - "log" - "mvvasilev/last_light/game" - "mvvasilev/last_light/render" - "os" - - "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" -) +import "mvvasilev/last_light/game" func main() { - - c, err := render.CreateRenderContext() - - if err != nil { - log.Fatalf("%~v", err) - } - - g := game.CreateGame() - - c.HandleInput(func(ev *tcell.EventKey) { - if ev.Key() == tcell.KeyCtrlC { - c.Stop() - os.Exit(0) - } - - g.Input(ev) - }) - - defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset) - - c.HandleRender(func(view views.View, deltaTime int64) { - fps := 1_000_000 / deltaTime - - fpsText := render.CreateText(0, 0, 16, 1, fmt.Sprintf("%v FPS", fps), defStyle) - - keepGoing := g.Tick(deltaTime) - - if !keepGoing { - c.Stop() - os.Exit(0) - } - - g.Draw(view) - - fpsText.Draw(view) - }) - - c.BeginRendering() + gc := game.CreateGameContext() + gc.Run() } diff --git a/render/arbitrary_drawable.go b/render/arbitrary_drawable.go new file mode 100644 index 0000000..9119b9c --- /dev/null +++ b/render/arbitrary_drawable.go @@ -0,0 +1,28 @@ +package render + +import ( + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type ArbitraryDrawable struct { + id uuid.UUID + drawingInstructions func(v views.View) +} + +func CreateDrawingInstructions(instructions func(v views.View)) *ArbitraryDrawable { + a := new(ArbitraryDrawable) + + a.id = uuid.New() + a.drawingInstructions = instructions + + return a +} + +func (ab *ArbitraryDrawable) UniqueId() uuid.UUID { + return ab.id +} + +func (ab *ArbitraryDrawable) Draw(v views.View) { + ab.drawingInstructions(v) +} diff --git a/render/grid.go b/render/grid.go index b21097c..c2237cd 100644 --- a/render/grid.go +++ b/render/grid.go @@ -12,8 +12,8 @@ type Grid struct { id uuid.UUID internalCellSize util.Size - numCellsHorizontal uint16 - numCellsVertical uint16 + numCellsHorizontal int + numCellsVertical int position util.Position style tcell.Style @@ -40,9 +40,9 @@ type Grid struct { } func CreateSimpleGrid( - x, y uint16, - cellWidth, cellHeight uint16, - numCellsHorizontal, numCellsVertical uint16, + x, y int, + cellWidth, cellHeight int, + numCellsHorizontal, numCellsVertical int, borderRune, fillRune rune, style tcell.Style, ) Grid { @@ -61,12 +61,12 @@ func CreateSimpleGrid( // '├', '─', '┼', '┤', // '└', '─', '┴', '┘', func CreateGrid( - x uint16, - y uint16, - cellWidth uint16, - cellHeight uint16, - numCellsHorizontal uint16, - numCellsVertical uint16, + x int, + y int, + cellWidth int, + cellHeight int, + numCellsHorizontal int, + numCellsVertical int, nwCorner, northBorder, verticalDownwardsTJunction, neCorner, westBorder, fillRune, internalVerticalBorder, eastBorder, horizontalRightTJunction, internalHorizontalBorder, crossJunction, horizontalLeftTJunction, diff --git a/render/raw.go b/render/raw.go index 63c810b..e200682 100644 --- a/render/raw.go +++ b/render/raw.go @@ -15,7 +15,7 @@ type Raw struct { style tcell.Style } -func CreateRawDrawable(x, y uint16, style tcell.Style, buffer ...string) *Raw { +func CreateRawDrawable(x, y int, style tcell.Style, buffer ...string) *Raw { r := new(Raw) r.position = util.PositionAt(x, y) @@ -30,10 +30,43 @@ func CreateRawDrawable(x, y uint16, style tcell.Style, buffer ...string) *Raw { return r } +func CreateRawDrawableFromBuffer(x, y int, style tcell.Style, buffer [][]rune) *Raw { + r := new(Raw) + + r.position = util.PositionAt(x, y) + r.buffer = buffer + + return r +} + func (r *Raw) UniqueId() uuid.UUID { return r.id } +func (r *Raw) DrawWithin(screenX, screenY, originX, originY, width, height int, v views.View) { + for h := originY; h < originY+height; h++ { + + if h < 0 || h >= len(r.buffer) { + screenY += 1 + continue + } + + for w := originX; w < originX+width; w++ { + if w < 0 || w >= len(r.buffer[h]) { + screenX += 1 + continue + } + + v.SetContent(screenX, screenY, r.buffer[h][w], nil, r.style) + + screenX += 1 + } + + screenX = 0 + screenY += 1 + } +} + func (r *Raw) Draw(v views.View) { x := r.position.X() y := r.position.Y() diff --git a/render/rectangle.go b/render/rectangle.go index b550be6..fb7753c 100644 --- a/render/rectangle.go +++ b/render/rectangle.go @@ -31,7 +31,7 @@ type Rectangle struct { fillRune rune } -func CreateBorderlessRectangle(x, y uint16, width, height uint16, fillRune rune, style tcell.Style) Rectangle { +func CreateBorderlessRectangle(x, y int, width, height int, fillRune rune, style tcell.Style) Rectangle { return CreateRectangle( x, y, width, height, 0, 0, 0, @@ -41,7 +41,7 @@ func CreateBorderlessRectangle(x, y uint16, width, height uint16, fillRune rune, ) } -func CreateSimpleEmptyRectangle(x, y uint16, width, height uint16, borderRune rune, style tcell.Style) Rectangle { +func CreateSimpleEmptyRectangle(x, y int, width, height int, borderRune rune, style tcell.Style) Rectangle { return CreateRectangle( x, y, width, height, borderRune, borderRune, borderRune, @@ -51,7 +51,7 @@ func CreateSimpleEmptyRectangle(x, y uint16, width, height uint16, borderRune ru ) } -func CreateSimpleRectangle(x uint16, y uint16, width uint16, height uint16, borderRune rune, fillRune rune, style tcell.Style) Rectangle { +func CreateSimpleRectangle(x int, y int, width int, height int, borderRune rune, fillRune rune, style tcell.Style) Rectangle { return CreateRectangle( x, y, width, height, borderRune, borderRune, borderRune, @@ -62,7 +62,7 @@ func CreateSimpleRectangle(x uint16, y uint16, width uint16, height uint16, bord } func CreateRectangleV2( - x, y uint16, width, height uint16, + x, y int, width, height int, upper, middle, lower string, isBorderless, isFilled bool, style tcell.Style, @@ -91,10 +91,10 @@ func CreateRectangleV2( // // ) func CreateRectangle( - x uint16, - y uint16, - width uint16, - height uint16, + x int, + y int, + width int, + height int, nwCorner, northBorder, neCorner, westBorder, fillRune, eastBorder, swCorner, southBorder, seCorner rune, diff --git a/render/context.go b/render/render_context.go similarity index 51% rename from render/context.go rename to render/render_context.go index dbd243d..0e66176 100644 --- a/render/context.go +++ b/render/render_context.go @@ -2,8 +2,8 @@ package render import ( "errors" + "fmt" "log" - "time" "github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2/views" @@ -22,39 +22,51 @@ type Drawable interface { Draw(v views.View) } +func Multidraw(drawables ...Drawable) []Drawable { + arr := make([]Drawable, 0) + + if drawables == nil { + return arr + } + + for _, d := range drawables { + if d == nil { + continue + } + + arr = append(arr, d) + } + + return arr +} + type RenderContext struct { - screen tcell.Screen - view *views.ViewPort - defaultStyle tcell.Style + screen tcell.Screen + view *views.ViewPort - events chan tcell.Event - quit chan struct{} - - lastRenderTime time.Time - - renderHandler func(view views.View, deltaTime int64) - inputHandler func(ev *tcell.EventKey) + events chan tcell.Event + quit chan struct{} + drawables chan Drawable } func CreateRenderContext() (*RenderContext, error) { - s, err := tcell.NewScreen() + screen, sErr := tcell.NewScreen() - if err != nil { - log.Fatal(err) - return nil, err + if sErr != nil { + log.Fatalf("%~v", sErr) } stopScreen := func() { - s.Fini() + screen.Fini() } - if err := s.Init(); err != nil { + if err := screen.Init(); err != nil { stopScreen() log.Fatal(err) return nil, err } - width, height := s.Size() + width, height := screen.Size() if width < TERMINAL_SIZE_WIDTH || height < TERMINAL_SIZE_HEIGHT { stopScreen() @@ -63,27 +75,25 @@ func CreateRenderContext() (*RenderContext, error) { } view := views.NewViewPort( - s, + screen, (width/2)-(TERMINAL_SIZE_WIDTH/2), (height/2)-(TERMINAL_SIZE_HEIGHT/2), TERMINAL_SIZE_WIDTH, TERMINAL_SIZE_HEIGHT, ) - defStyle := tcell.StyleDefault.Background(DEFAULT_STYLE_BACKGROUND).Foreground(DEFAULT_STYLE_FOREGROUND) - events := make(chan tcell.Event) quit := make(chan struct{}) - go s.ChannelEvents(events, quit) + go screen.ChannelEvents(events, quit) context := new(RenderContext) - context.screen = s - context.defaultStyle = defStyle + context.screen = screen context.events = events context.quit = quit context.view = view + context.drawables = make(chan Drawable) return context, nil } @@ -92,12 +102,31 @@ func (c *RenderContext) Stop() { c.screen.Fini() } -func (c *RenderContext) HandleRender(renderHandler func(view views.View, deltaTime int64)) { - c.renderHandler = renderHandler +func (c *RenderContext) CollectInputEvents() []*tcell.EventKey { + events := make([]tcell.Event, len(c.events)) + + select { + case e := <-c.events: + events = append(events, e) + default: + } + + inputEvents := make([]*tcell.EventKey, 0, len(events)) + + for _, e := range events { + switch ev := e.(type) { + case *tcell.EventKey: + inputEvents = append(inputEvents, ev) + case *tcell.EventResize: + c.onResize(ev) + } + } + + return inputEvents } -func (c *RenderContext) HandleInput(inputHandler func(ev *tcell.EventKey)) { - c.inputHandler = inputHandler +func (c *RenderContext) DrawableQueue() chan Drawable { + return c.drawables } func (c *RenderContext) onResize(ev *tcell.EventResize) { @@ -115,33 +144,18 @@ func (c *RenderContext) onResize(ev *tcell.EventResize) { c.screen.Sync() } -func (c *RenderContext) BeginRendering() { - c.lastRenderTime = time.Now() +func (c *RenderContext) Draw(deltaTime int64, drawables []Drawable) { + fps := 1_000_000 / deltaTime - for { - deltaTime := 1 + time.Since(c.lastRenderTime).Microseconds() - c.lastRenderTime = time.Now() + c.view.Clear() - c.screen.Clear() + fpsText := CreateText(0, 0, 16, 1, fmt.Sprintf("%v FPS", fps), tcell.StyleDefault) - c.renderHandler(c.view, deltaTime) - - c.screen.Show() - - select { - case ev, ok := <-c.events: - - if !ok { - break - } - - switch ev := ev.(type) { - case *tcell.EventResize: - c.onResize(ev) - case *tcell.EventKey: - c.inputHandler(ev) - } - default: - } + for _, d := range drawables { + d.Draw(c.view) } + + fpsText.Draw(c.view) + + c.screen.Show() } diff --git a/render/text.go b/render/text.go index 06552f2..4746c27 100644 --- a/render/text.go +++ b/render/text.go @@ -19,8 +19,8 @@ type Text struct { } func CreateText( - x, y uint16, - width, height uint16, + x, y int, + width, height int, content string, style tcell.Style, ) *Text { diff --git a/render/viewport.go b/render/viewport.go new file mode 100644 index 0000000..738df65 --- /dev/null +++ b/render/viewport.go @@ -0,0 +1,96 @@ +package render + +import ( + "mvvasilev/last_light/util" + + "github.com/gdamore/tcell/v2" + "github.com/gdamore/tcell/v2/views" + "github.com/google/uuid" +) + +type Viewport struct { + id uuid.UUID + screenLocation util.Position + + viewportCenter util.Position + viewportSize util.Size + + style tcell.Style +} + +func CreateViewport(screenLoc, viewportCenter util.Position, size util.Size, style tcell.Style) *Viewport { + v := new(Viewport) + + v.id = uuid.New() + v.screenLocation = screenLoc + v.viewportCenter = viewportCenter + v.viewportSize = size + v.style = style + + return v +} + +func (vp *Viewport) UniqueId() uuid.UUID { + return vp.id +} + +func (vp *Viewport) Center() util.Position { + return vp.viewportCenter +} + +func (vp *Viewport) SetCenter(pos util.Position) { + vp.viewportCenter = pos +} + +func (vp *Viewport) Size() util.Size { + return vp.viewportSize +} + +func (vp *Viewport) ScreenLocation() util.Position { + return vp.screenLocation +} + +func (vp *Viewport) DrawFromProvider(v views.View, provider func(x, y int) rune) { + width, height := vp.viewportSize.WH() + originX, originY := vp.viewportCenter.WithOffset(-width/2, -height/2).XY() + screenX, screenY := vp.screenLocation.XY() + + for h := originY; h < originY+height; h++ { + for w := originX; w < originX+width; w++ { + v.SetContent(screenX, screenY, provider(w, h), nil, vp.style) + + screenX += 1 + } + + screenX = 0 + screenY += 1 + } +} + +func (vp *Viewport) Draw(v views.View, buffer [][]rune) { + width, height := vp.viewportSize.WH() + originX, originY := vp.viewportCenter.WithOffset(-width/2, -height/2).XY() + screenX, screenY := vp.screenLocation.XY() + + for h := originY; h < originY+height; h++ { + + if h < 0 || h >= len(buffer) { + screenY += 1 + continue + } + + for w := originX; w < originX+width; w++ { + if w < 0 || w >= len(buffer[h]) { + screenX += 1 + continue + } + + v.SetContent(screenX, screenY, buffer[h][w], nil, vp.style) + + screenX += 1 + } + + screenX = 0 + screenY += 1 + } +} diff --git a/ui/borderButton.go b/ui/border_button.go similarity index 95% rename from ui/borderButton.go rename to ui/border_button.go index bb45d7f..74c7fcf 100644 --- a/ui/borderButton.go +++ b/ui/border_button.go @@ -41,7 +41,7 @@ func (b *UIBorderedButton) UniqueId() uuid.UUID { return b.id } -func (b *UIBorderedButton) MoveTo(x uint16, y uint16) { +func (b *UIBorderedButton) MoveTo(x int, y int) { panic("not implemented") // TODO: Implement } diff --git a/ui/container.go b/ui/container.go index 4de591a..c955d38 100644 --- a/ui/container.go +++ b/ui/container.go @@ -33,7 +33,7 @@ type UIContainer struct { elements []UIElement } -func CreateUIContainer(x, y uint16, width, height uint16, layout UIContainerLayout) *UIContainer { +func CreateUIContainer(x, y int, width, height int, layout UIContainerLayout) *UIContainer { container := new(UIContainer) container.id = uuid.New() @@ -57,7 +57,7 @@ func (uic *UIContainer) UniqueId() uuid.UUID { return uic.id } -func (uic *UIContainer) MoveTo(x, y uint16) { +func (uic *UIContainer) MoveTo(x, y int) { uic.position = util.PositionAt(x, y) } diff --git a/ui/label.go b/ui/label.go index 0f828c7..f0ad08d 100644 --- a/ui/label.go +++ b/ui/label.go @@ -15,7 +15,7 @@ type UILabel struct { text *render.Text } -func CreateUILabel(x, y uint16, width, height uint16, content string, style tcell.Style) *UILabel { +func CreateUILabel(x, y int, width, height int, content string, style tcell.Style) *UILabel { label := new(UILabel) label.id = uuid.New() @@ -24,11 +24,11 @@ func CreateUILabel(x, y uint16, width, height uint16, content string, style tcel return label } -func CreateSingleLineUILabel(x, y uint16, content string, style tcell.Style) *UILabel { +func CreateSingleLineUILabel(x, y int, content string, style tcell.Style) *UILabel { label := new(UILabel) label.id = uuid.New() - label.text = render.CreateText(x, y, uint16(utf8.RuneCountInString(content)), 1, content, style) + label.text = render.CreateText(x, y, int(utf8.RuneCountInString(content)), 1, content, style) return label } @@ -37,8 +37,8 @@ func (t *UILabel) UniqueId() uuid.UUID { return t.id } -func (t *UILabel) MoveTo(x uint16, y uint16) { - t.text = render.CreateText(x, y, uint16(t.text.Size().Width()), uint16(t.Size().Height()), t.text.Content(), t.text.Style()) +func (t *UILabel) MoveTo(x int, y int) { + t.text = render.CreateText(x, y, int(t.text.Size().Width()), int(t.Size().Height()), t.text.Content(), t.text.Style()) } func (t *UILabel) Position() util.Position { diff --git a/ui/simpleButton.go b/ui/simple_button.go similarity index 73% rename from ui/simpleButton.go rename to ui/simple_button.go index b84af8b..9f5502e 100644 --- a/ui/simpleButton.go +++ b/ui/simple_button.go @@ -20,11 +20,11 @@ type UISimpleButton struct { highlightedStyle tcell.Style } -func CreateSimpleButton(x, y uint16, text string, unhighlightedStyle, highlightedStyle tcell.Style, onSelect func()) *UISimpleButton { +func CreateSimpleButton(x, y int, text string, unhighlightedStyle, highlightedStyle tcell.Style, onSelect func()) *UISimpleButton { sb := new(UISimpleButton) sb.id = uuid.New() - sb.text = render.CreateText(x, y, uint16(utf8.RuneCountInString(text)), 1, text, unhighlightedStyle) + sb.text = render.CreateText(x, y, int(utf8.RuneCountInString(text)), 1, text, unhighlightedStyle) sb.isHighlighted = false sb.selectHandler = onSelect sb.highlightedStyle = highlightedStyle @@ -51,8 +51,8 @@ func (sb *UISimpleButton) Highlight() { newContent := "[ " + sb.text.Content() + " ]" sb.text = render.CreateText( - uint16(sb.Position().X()-2), uint16(sb.Position().Y()), - uint16(utf8.RuneCountInString(newContent)), 1, + int(sb.Position().X()-2), int(sb.Position().Y()), + int(utf8.RuneCountInString(newContent)), 1, newContent, sb.highlightedStyle, ) @@ -66,8 +66,8 @@ func (sb *UISimpleButton) Unhighlight() { contentLen := utf8.RuneCountInString(content) sb.text = render.CreateText( - uint16(sb.Position().X()+2), uint16(sb.Position().Y()), - uint16(contentLen), 1, + int(sb.Position().X()+2), int(sb.Position().Y()), + int(contentLen), 1, content, sb.unhighlightedStyle, ) @@ -81,8 +81,8 @@ func (sb *UISimpleButton) UniqueId() uuid.UUID { return sb.id } -func (sb *UISimpleButton) MoveTo(x uint16, y uint16) { - sb.text = render.CreateText(x, y, uint16(utf8.RuneCountInString(sb.text.Content())), 1, sb.text.Content(), sb.highlightedStyle) +func (sb *UISimpleButton) MoveTo(x int, y int) { + sb.text = render.CreateText(x, y, int(utf8.RuneCountInString(sb.text.Content())), 1, sb.text.Content(), sb.highlightedStyle) } func (sb *UISimpleButton) Position() util.Position { diff --git a/ui/ui.go b/ui/ui.go index 0b5f39b..e025921 100644 --- a/ui/ui.go +++ b/ui/ui.go @@ -1,20 +1,19 @@ package ui import ( + "mvvasilev/last_light/render" "mvvasilev/last_light/util" "github.com/gdamore/tcell/v2" - "github.com/gdamore/tcell/v2/views" - "github.com/google/uuid" ) type UIElement interface { - UniqueId() uuid.UUID - MoveTo(x, y uint16) + MoveTo(x, y int) Position() util.Position Size() util.Size - Draw(v views.View) Input(e *tcell.EventKey) + + render.Drawable } type UIHighlightableElement interface { diff --git a/ui/window.go b/ui/window.go index 1f36fab..a20cab0 100644 --- a/ui/window.go +++ b/ui/window.go @@ -17,14 +17,14 @@ type UIWindow struct { box render.Rectangle } -func CreateWindow(x, y, width, height uint16, title string, style tcell.Style) *UIWindow { +func CreateWindow(x, y, width, height int, title string, style tcell.Style) *UIWindow { w := new(UIWindow) titleLen := utf8.RuneCountInString(title) - titlePos := (width / 2) - uint16(titleLen/2) + titlePos := (width / 2) - int(titleLen/2) - w.title = render.CreateText(x+titlePos, y, uint16(titleLen), 1, title, style) + w.title = render.CreateText(x+titlePos, y, int(titleLen), 1, title, style) w.box = render.CreateRectangle( x, y, width, height, @@ -41,7 +41,7 @@ func (w *UIWindow) UniqueId() uuid.UUID { return w.id } -func (w *UIWindow) MoveTo(x uint16, y uint16) { +func (w *UIWindow) MoveTo(x int, y int) { } diff --git a/util/util.go b/util/util.go index 23aa9e0..feb68ef 100644 --- a/util/util.go +++ b/util/util.go @@ -5,7 +5,7 @@ type Position struct { y int } -func PositionAt(x uint16, y uint16) Position { +func PositionAt(x int, y int) Position { return Position{int(x), int(y)} } @@ -21,8 +21,10 @@ func (p Position) XY() (int, int) { return p.x, p.y } -func (p Position) XYUint16() (uint16, uint16) { - return uint16(p.x), uint16(p.y) +func (p Position) WithOffset(xOffset int, yOffset int) Position { + p.x = p.x + xOffset + p.y = p.y + yOffset + return p } type Size struct { @@ -30,10 +32,14 @@ type Size struct { height int } -func SizeOf(width uint16, height uint16) Size { +func SizeOf(width int, height int) Size { return Size{int(width), int(height)} } +func SizeOfInt(width int, height int) Size { + return Size{width, height} +} + func (s Size) Width() int { return s.width } @@ -42,8 +48,8 @@ func (s Size) Height() int { return s.height } -func (s Size) WHUint16() (uint16, uint16) { - return uint16(s.width), uint16(s.height) +func (s Size) WH() (int, int) { + return s.width, s.height } func LimitIncrement(i int, limit int) int {