New drawing system, start of inventory menu

This commit is contained in:
Miroslav Vasilev 2024-04-27 22:32:05 +03:00
parent c0f80f0e0c
commit 099155c186
32 changed files with 890 additions and 278 deletions

View file

@ -2,13 +2,15 @@ package game
import ( import (
"mvvasilev/last_light/game/state" "mvvasilev/last_light/game/state"
"mvvasilev/last_light/render"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
) )
type Game struct { type Game struct {
state state.GameState state state.GameState
quitGame bool
} }
func CreateGame() *Game { func CreateGame() *Game {
@ -20,10 +22,18 @@ func CreateGame() *Game {
} }
func (g *Game) Input(ev *tcell.EventKey) { func (g *Game) Input(ev *tcell.EventKey) {
if ev.Key() == tcell.KeyCtrlC {
g.quitGame = true
}
g.state.OnInput(ev) g.state.OnInput(ev)
} }
func (g *Game) Tick(dt int64) bool { func (g *Game) Tick(dt int64) bool {
if g.quitGame {
return false
}
s := g.state.OnTick(dt) s := g.state.OnTick(dt)
switch s.(type) { switch s.(type) {
@ -36,6 +46,6 @@ func (g *Game) Tick(dt int64) bool {
return true return true
} }
func (g *Game) Draw(v views.View) { func (g *Game) CollectDrawables() []render.Drawable {
g.state.OnDraw(v) return g.state.CollectDrawables()
} }

55
game/game_context.go Normal file
View file

@ -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)
}
}

1
game/model/dungeon.go Normal file
View file

@ -0,0 +1 @@
package model

View file

@ -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() {
}

View file

@ -4,22 +4,36 @@ import (
"mvvasilev/last_light/util" "mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid" "github.com/google/uuid"
) )
type Direction int type Direction int
const ( const (
Up Direction = iota DirectionNone Direction = iota
Down DirectionUp
Left DirectionDown
Right 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 { type Entity interface {
UniqueId() uuid.UUID UniqueId() uuid.UUID
Draw(v views.View)
Input(e *tcell.EventKey) Input(e *tcell.EventKey)
Tick(dt int64) Tick(dt int64)
} }

View file

@ -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() {
}

12
game/model/map.go Normal file
View file

@ -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()
}

View file

@ -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()
}
}

View file

@ -4,17 +4,15 @@ import (
"mvvasilev/last_light/util" "mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid" "github.com/google/uuid"
) )
type Player struct { type Player struct {
id uuid.UUID id uuid.UUID
position util.Position 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 := new(Player)
p.id = uuid.New() p.id = uuid.New()
@ -27,48 +25,23 @@ func (p *Player) UniqueId() uuid.UUID {
return p.id return p.id
} }
func (p *Player) Move(dir Direction) { func (p *Player) Position() util.Position {
x, y := p.position.XYUint16() return p.position
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) Draw(v views.View) { func (p *Player) Move(dir Direction) {
x, y := p.position.XY() p.position = p.Position().WithOffset(MovementDirectionOffset(dir))
v.SetContent(x, y, '@', nil, p.style) }
func (p *Player) Presentation() rune {
return '@'
}
func (p *Player) Passable() bool {
return false
} }
func (p *Player) Input(e *tcell.EventKey) { 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) { func (p *Player) Tick(dt int64) {

86
game/model/tile.go Normal file
View file

@ -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
}

View file

@ -1,14 +1,15 @@
package state package state
import ( import (
"mvvasilev/last_light/render"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
) )
type GameState interface { type GameState interface {
OnInput(e *tcell.EventKey) OnInput(e *tcell.EventKey)
OnTick(dt int64) GameState OnTick(dt int64) GameState
OnDraw(c views.View) CollectDrawables() []render.Drawable
} }
type PausableState interface { type PausableState interface {

View file

@ -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,
)
}

View file

@ -6,7 +6,6 @@ import (
"mvvasilev/last_light/util" "mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
) )
type MainMenuState struct { type MainMenuState struct {
@ -78,10 +77,14 @@ func (mms *MainMenuState) OnTick(dt int64) GameState {
return mms return mms
} }
func (mms *MainMenuState) OnDraw(c views.View) { func (mms *MainMenuState) CollectDrawables() []render.Drawable {
mms.menuTitle.Draw(c) arr := make([]render.Drawable, 0)
arr = append(arr, mms.menuTitle)
for _, b := range mms.buttons { for _, b := range mms.buttons {
b.Draw(c) arr = append(arr, b)
} }
return arr
} }

View file

@ -6,7 +6,6 @@ import (
"mvvasilev/last_light/util" "mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
) )
type PauseGameState struct { type PauseGameState struct {
@ -27,13 +26,13 @@ func PauseGame(prevState PausableState) *PauseGameState {
highlightStyle := tcell.StyleDefault.Attributes(tcell.AttrBold) 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 = make([]*ui.UISimpleButton, 0)
s.buttons = append( s.buttons = append(
s.buttons, s.buttons,
ui.CreateSimpleButton( ui.CreateSimpleButton(
uint16(s.pauseMenuWindow.Position().X())+3, int(s.pauseMenuWindow.Position().X())+3,
uint16(s.pauseMenuWindow.Position().Y())+1, int(s.pauseMenuWindow.Position().Y())+1,
"Resume", "Resume",
tcell.StyleDefault, tcell.StyleDefault,
highlightStyle, highlightStyle,
@ -45,8 +44,8 @@ func PauseGame(prevState PausableState) *PauseGameState {
s.buttons = append( s.buttons = append(
s.buttons, s.buttons,
ui.CreateSimpleButton( ui.CreateSimpleButton(
uint16(s.pauseMenuWindow.Position().X())+3, int(s.pauseMenuWindow.Position().X())+3,
uint16(s.pauseMenuWindow.Position().Y())+3, int(s.pauseMenuWindow.Position().Y())+3,
"Exit To Main Menu", "Exit To Main Menu",
tcell.StyleDefault, tcell.StyleDefault,
highlightStyle, highlightStyle,
@ -97,12 +96,16 @@ func (pg *PauseGameState) OnTick(dt int64) GameState {
return pg return pg
} }
func (pg *PauseGameState) OnDraw(c views.View) { func (pg *PauseGameState) CollectDrawables() []render.Drawable {
pg.prevState.OnDraw(c) 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 { for _, b := range pg.buttons {
b.Draw(c) arr = append(arr, b)
} }
return arr
} }

View file

@ -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)
}

144
game/state/playing_state.go Normal file
View file

@ -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 ' '
})
}))
}

View file

@ -1,8 +1,9 @@
package state package state
import ( import (
"mvvasilev/last_light/render"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
) )
type QuitState struct { type QuitState struct {
@ -16,6 +17,6 @@ func (q *QuitState) OnTick(dt int64) GameState {
return q return q
} }
func (q *QuitState) OnDraw(c views.View) { func (q *QuitState) CollectDrawables() []render.Drawable {
return render.Multidraw(nil)
} }

51
main.go
View file

@ -1,53 +1,8 @@
package main package main
import ( import "mvvasilev/last_light/game"
"fmt"
"log"
"mvvasilev/last_light/game"
"mvvasilev/last_light/render"
"os"
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
)
func main() { func main() {
gc := game.CreateGameContext()
c, err := render.CreateRenderContext() gc.Run()
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()
} }

View file

@ -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)
}

View file

@ -12,8 +12,8 @@ type Grid struct {
id uuid.UUID id uuid.UUID
internalCellSize util.Size internalCellSize util.Size
numCellsHorizontal uint16 numCellsHorizontal int
numCellsVertical uint16 numCellsVertical int
position util.Position position util.Position
style tcell.Style style tcell.Style
@ -40,9 +40,9 @@ type Grid struct {
} }
func CreateSimpleGrid( func CreateSimpleGrid(
x, y uint16, x, y int,
cellWidth, cellHeight uint16, cellWidth, cellHeight int,
numCellsHorizontal, numCellsVertical uint16, numCellsHorizontal, numCellsVertical int,
borderRune, fillRune rune, borderRune, fillRune rune,
style tcell.Style, style tcell.Style,
) Grid { ) Grid {
@ -61,12 +61,12 @@ func CreateSimpleGrid(
// '├', '─', '┼', '┤', // '├', '─', '┼', '┤',
// '└', '─', '┴', '┘', // '└', '─', '┴', '┘',
func CreateGrid( func CreateGrid(
x uint16, x int,
y uint16, y int,
cellWidth uint16, cellWidth int,
cellHeight uint16, cellHeight int,
numCellsHorizontal uint16, numCellsHorizontal int,
numCellsVertical uint16, numCellsVertical int,
nwCorner, northBorder, verticalDownwardsTJunction, neCorner, nwCorner, northBorder, verticalDownwardsTJunction, neCorner,
westBorder, fillRune, internalVerticalBorder, eastBorder, westBorder, fillRune, internalVerticalBorder, eastBorder,
horizontalRightTJunction, internalHorizontalBorder, crossJunction, horizontalLeftTJunction, horizontalRightTJunction, internalHorizontalBorder, crossJunction, horizontalLeftTJunction,

View file

@ -15,7 +15,7 @@ type Raw struct {
style tcell.Style 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 := new(Raw)
r.position = util.PositionAt(x, y) r.position = util.PositionAt(x, y)
@ -30,10 +30,43 @@ func CreateRawDrawable(x, y uint16, style tcell.Style, buffer ...string) *Raw {
return r 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 { func (r *Raw) UniqueId() uuid.UUID {
return r.id 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) { func (r *Raw) Draw(v views.View) {
x := r.position.X() x := r.position.X()
y := r.position.Y() y := r.position.Y()

View file

@ -31,7 +31,7 @@ type Rectangle struct {
fillRune rune 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( return CreateRectangle(
x, y, width, height, x, y, width, height,
0, 0, 0, 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( return CreateRectangle(
x, y, width, height, x, y, width, height,
borderRune, borderRune, borderRune, 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( return CreateRectangle(
x, y, width, height, x, y, width, height,
borderRune, borderRune, borderRune, borderRune, borderRune, borderRune,
@ -62,7 +62,7 @@ func CreateSimpleRectangle(x uint16, y uint16, width uint16, height uint16, bord
} }
func CreateRectangleV2( func CreateRectangleV2(
x, y uint16, width, height uint16, x, y int, width, height int,
upper, middle, lower string, upper, middle, lower string,
isBorderless, isFilled bool, isBorderless, isFilled bool,
style tcell.Style, style tcell.Style,
@ -91,10 +91,10 @@ func CreateRectangleV2(
// //
// ) // )
func CreateRectangle( func CreateRectangle(
x uint16, x int,
y uint16, y int,
width uint16, width int,
height uint16, height int,
nwCorner, northBorder, neCorner, nwCorner, northBorder, neCorner,
westBorder, fillRune, eastBorder, westBorder, fillRune, eastBorder,
swCorner, southBorder, seCorner rune, swCorner, southBorder, seCorner rune,

View file

@ -2,8 +2,8 @@ package render
import ( import (
"errors" "errors"
"fmt"
"log" "log"
"time"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views" "github.com/gdamore/tcell/v2/views"
@ -22,39 +22,51 @@ type Drawable interface {
Draw(v views.View) 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 { type RenderContext struct {
screen tcell.Screen screen tcell.Screen
view *views.ViewPort view *views.ViewPort
defaultStyle tcell.Style
events chan tcell.Event events chan tcell.Event
quit chan struct{} quit chan struct{}
drawables chan Drawable
lastRenderTime time.Time
renderHandler func(view views.View, deltaTime int64)
inputHandler func(ev *tcell.EventKey)
} }
func CreateRenderContext() (*RenderContext, error) { func CreateRenderContext() (*RenderContext, error) {
s, err := tcell.NewScreen() screen, sErr := tcell.NewScreen()
if err != nil { if sErr != nil {
log.Fatal(err) log.Fatalf("%~v", sErr)
return nil, err
} }
stopScreen := func() { stopScreen := func() {
s.Fini() screen.Fini()
} }
if err := s.Init(); err != nil { if err := screen.Init(); err != nil {
stopScreen() stopScreen()
log.Fatal(err) log.Fatal(err)
return nil, err return nil, err
} }
width, height := s.Size() width, height := screen.Size()
if width < TERMINAL_SIZE_WIDTH || height < TERMINAL_SIZE_HEIGHT { if width < TERMINAL_SIZE_WIDTH || height < TERMINAL_SIZE_HEIGHT {
stopScreen() stopScreen()
@ -63,27 +75,25 @@ func CreateRenderContext() (*RenderContext, error) {
} }
view := views.NewViewPort( view := views.NewViewPort(
s, screen,
(width/2)-(TERMINAL_SIZE_WIDTH/2), (width/2)-(TERMINAL_SIZE_WIDTH/2),
(height/2)-(TERMINAL_SIZE_HEIGHT/2), (height/2)-(TERMINAL_SIZE_HEIGHT/2),
TERMINAL_SIZE_WIDTH, TERMINAL_SIZE_WIDTH,
TERMINAL_SIZE_HEIGHT, TERMINAL_SIZE_HEIGHT,
) )
defStyle := tcell.StyleDefault.Background(DEFAULT_STYLE_BACKGROUND).Foreground(DEFAULT_STYLE_FOREGROUND)
events := make(chan tcell.Event) events := make(chan tcell.Event)
quit := make(chan struct{}) quit := make(chan struct{})
go s.ChannelEvents(events, quit) go screen.ChannelEvents(events, quit)
context := new(RenderContext) context := new(RenderContext)
context.screen = s context.screen = screen
context.defaultStyle = defStyle
context.events = events context.events = events
context.quit = quit context.quit = quit
context.view = view context.view = view
context.drawables = make(chan Drawable)
return context, nil return context, nil
} }
@ -92,12 +102,31 @@ func (c *RenderContext) Stop() {
c.screen.Fini() c.screen.Fini()
} }
func (c *RenderContext) HandleRender(renderHandler func(view views.View, deltaTime int64)) { func (c *RenderContext) CollectInputEvents() []*tcell.EventKey {
c.renderHandler = renderHandler 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)) { func (c *RenderContext) DrawableQueue() chan Drawable {
c.inputHandler = inputHandler return c.drawables
} }
func (c *RenderContext) onResize(ev *tcell.EventResize) { func (c *RenderContext) onResize(ev *tcell.EventResize) {
@ -115,33 +144,18 @@ func (c *RenderContext) onResize(ev *tcell.EventResize) {
c.screen.Sync() c.screen.Sync()
} }
func (c *RenderContext) BeginRendering() { func (c *RenderContext) Draw(deltaTime int64, drawables []Drawable) {
c.lastRenderTime = time.Now() fps := 1_000_000 / deltaTime
for { c.view.Clear()
deltaTime := 1 + time.Since(c.lastRenderTime).Microseconds()
c.lastRenderTime = time.Now()
c.screen.Clear() fpsText := CreateText(0, 0, 16, 1, fmt.Sprintf("%v FPS", fps), tcell.StyleDefault)
c.renderHandler(c.view, deltaTime) for _, d := range drawables {
d.Draw(c.view)
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:
}
} }
fpsText.Draw(c.view)
c.screen.Show()
} }

View file

@ -19,8 +19,8 @@ type Text struct {
} }
func CreateText( func CreateText(
x, y uint16, x, y int,
width, height uint16, width, height int,
content string, content string,
style tcell.Style, style tcell.Style,
) *Text { ) *Text {

96
render/viewport.go Normal file
View file

@ -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
}
}

View file

@ -41,7 +41,7 @@ func (b *UIBorderedButton) UniqueId() uuid.UUID {
return b.id return b.id
} }
func (b *UIBorderedButton) MoveTo(x uint16, y uint16) { func (b *UIBorderedButton) MoveTo(x int, y int) {
panic("not implemented") // TODO: Implement panic("not implemented") // TODO: Implement
} }

View file

@ -33,7 +33,7 @@ type UIContainer struct {
elements []UIElement 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 := new(UIContainer)
container.id = uuid.New() container.id = uuid.New()
@ -57,7 +57,7 @@ func (uic *UIContainer) UniqueId() uuid.UUID {
return uic.id return uic.id
} }
func (uic *UIContainer) MoveTo(x, y uint16) { func (uic *UIContainer) MoveTo(x, y int) {
uic.position = util.PositionAt(x, y) uic.position = util.PositionAt(x, y)
} }

View file

@ -15,7 +15,7 @@ type UILabel struct {
text *render.Text 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 := new(UILabel)
label.id = uuid.New() label.id = uuid.New()
@ -24,11 +24,11 @@ func CreateUILabel(x, y uint16, width, height uint16, content string, style tcel
return label 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 := new(UILabel)
label.id = uuid.New() 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 return label
} }
@ -37,8 +37,8 @@ func (t *UILabel) UniqueId() uuid.UUID {
return t.id return t.id
} }
func (t *UILabel) MoveTo(x uint16, y uint16) { func (t *UILabel) MoveTo(x int, y int) {
t.text = render.CreateText(x, y, uint16(t.text.Size().Width()), uint16(t.Size().Height()), t.text.Content(), t.text.Style()) 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 { func (t *UILabel) Position() util.Position {

View file

@ -20,11 +20,11 @@ type UISimpleButton struct {
highlightedStyle tcell.Style 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 := new(UISimpleButton)
sb.id = uuid.New() 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.isHighlighted = false
sb.selectHandler = onSelect sb.selectHandler = onSelect
sb.highlightedStyle = highlightedStyle sb.highlightedStyle = highlightedStyle
@ -51,8 +51,8 @@ func (sb *UISimpleButton) Highlight() {
newContent := "[ " + sb.text.Content() + " ]" newContent := "[ " + sb.text.Content() + " ]"
sb.text = render.CreateText( sb.text = render.CreateText(
uint16(sb.Position().X()-2), uint16(sb.Position().Y()), int(sb.Position().X()-2), int(sb.Position().Y()),
uint16(utf8.RuneCountInString(newContent)), 1, int(utf8.RuneCountInString(newContent)), 1,
newContent, newContent,
sb.highlightedStyle, sb.highlightedStyle,
) )
@ -66,8 +66,8 @@ func (sb *UISimpleButton) Unhighlight() {
contentLen := utf8.RuneCountInString(content) contentLen := utf8.RuneCountInString(content)
sb.text = render.CreateText( sb.text = render.CreateText(
uint16(sb.Position().X()+2), uint16(sb.Position().Y()), int(sb.Position().X()+2), int(sb.Position().Y()),
uint16(contentLen), 1, int(contentLen), 1,
content, content,
sb.unhighlightedStyle, sb.unhighlightedStyle,
) )
@ -81,8 +81,8 @@ func (sb *UISimpleButton) UniqueId() uuid.UUID {
return sb.id return sb.id
} }
func (sb *UISimpleButton) MoveTo(x uint16, y uint16) { func (sb *UISimpleButton) MoveTo(x int, y int) {
sb.text = render.CreateText(x, y, uint16(utf8.RuneCountInString(sb.text.Content())), 1, sb.text.Content(), sb.highlightedStyle) sb.text = render.CreateText(x, y, int(utf8.RuneCountInString(sb.text.Content())), 1, sb.text.Content(), sb.highlightedStyle)
} }
func (sb *UISimpleButton) Position() util.Position { func (sb *UISimpleButton) Position() util.Position {

View file

@ -1,20 +1,19 @@
package ui package ui
import ( import (
"mvvasilev/last_light/render"
"mvvasilev/last_light/util" "mvvasilev/last_light/util"
"github.com/gdamore/tcell/v2" "github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/views"
"github.com/google/uuid"
) )
type UIElement interface { type UIElement interface {
UniqueId() uuid.UUID MoveTo(x, y int)
MoveTo(x, y uint16)
Position() util.Position Position() util.Position
Size() util.Size Size() util.Size
Draw(v views.View)
Input(e *tcell.EventKey) Input(e *tcell.EventKey)
render.Drawable
} }
type UIHighlightableElement interface { type UIHighlightableElement interface {

View file

@ -17,14 +17,14 @@ type UIWindow struct {
box render.Rectangle 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) w := new(UIWindow)
titleLen := utf8.RuneCountInString(title) 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( w.box = render.CreateRectangle(
x, y, width, height, x, y, width, height,
@ -41,7 +41,7 @@ func (w *UIWindow) UniqueId() uuid.UUID {
return w.id return w.id
} }
func (w *UIWindow) MoveTo(x uint16, y uint16) { func (w *UIWindow) MoveTo(x int, y int) {
} }

View file

@ -5,7 +5,7 @@ type Position struct {
y int y int
} }
func PositionAt(x uint16, y uint16) Position { func PositionAt(x int, y int) Position {
return Position{int(x), int(y)} return Position{int(x), int(y)}
} }
@ -21,8 +21,10 @@ func (p Position) XY() (int, int) {
return p.x, p.y return p.x, p.y
} }
func (p Position) XYUint16() (uint16, uint16) { func (p Position) WithOffset(xOffset int, yOffset int) Position {
return uint16(p.x), uint16(p.y) p.x = p.x + xOffset
p.y = p.y + yOffset
return p
} }
type Size struct { type Size struct {
@ -30,10 +32,14 @@ type Size struct {
height int height int
} }
func SizeOf(width uint16, height uint16) Size { func SizeOf(width int, height int) Size {
return Size{int(width), int(height)} return Size{int(width), int(height)}
} }
func SizeOfInt(width int, height int) Size {
return Size{width, height}
}
func (s Size) Width() int { func (s Size) Width() int {
return s.width return s.width
} }
@ -42,8 +48,8 @@ func (s Size) Height() int {
return s.height return s.height
} }
func (s Size) WHUint16() (uint16, uint16) { func (s Size) WH() (int, int) {
return uint16(s.width), uint16(s.height) return s.width, s.height
} }
func LimitIncrement(i int, limit int) int { func LimitIncrement(i int, limit int) int {