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

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

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"
"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) {

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
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 {

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

View file

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

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

51
main.go
View file

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

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
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,

View file

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

View file

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

View file

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

View file

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

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
}
func (b *UIBorderedButton) MoveTo(x uint16, y uint16) {
func (b *UIBorderedButton) MoveTo(x int, y int) {
panic("not implemented") // TODO: Implement
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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