Layered container, rectangles, text, async event handling

This commit is contained in:
Miroslav Vasilev 2024-04-19 16:56:43 +03:00
parent b4ee5a7f13
commit 6d576939db
10 changed files with 529 additions and 7 deletions

16
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,16 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "${workspaceFolder}/main.go",
"console": "integratedTerminal"
}
]
}

30
DESIGN.md Normal file
View file

@ -0,0 +1,30 @@
# Last Light
- Roguelike RPG
- Inventory System
- Weapons & Armor
- Head
- Chest
- Leggings
- Feet
- Left Hand
- Right Hand
- Damage Types
- Physical
- Slashing
- Piercing
- Bludgeoning
- Magical
- Fire
- Cold
- Necrotic
- Acid
- Poison
- 9-level Dungeon
- 4 types of dungeon levels:
- Caverns ( Cellular Automata )
- Dungeon ( Maze w/ Rooms )
- Mine ( Broguelike )
- Underground City ( Caverns + Dungeon combo )
- Objective: Pick up the Last Light and bring it to its Altar ( Altar of the Last Light )
- The light is always on level 9, but the Altar can be anywhere

7
go.mod
View file

@ -2,9 +2,14 @@ module mvvasilev/last_light
go 1.22.2
require (
github.com/gdamore/tcell/v2 v2.7.4
github.com/tidwall/btree v1.7.0
github.com/google/uuid v1.6.0
)
require (
github.com/gdamore/encoding v1.0.0 // indirect
github.com/gdamore/tcell/v2 v2.7.4 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/rivo/uniseg v0.4.3 // indirect

4
go.sum
View file

@ -2,6 +2,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU=
github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
@ -9,6 +11,8 @@ github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw=
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

1
layer.go Normal file
View file

@ -0,0 +1 @@
package main

86
main.go
View file

@ -1,18 +1,92 @@
package main
import (
"io"
"fmt"
"log"
"mvvasilev/last_light/render"
"os"
"time"
"golang.org/x/term"
"github.com/gdamore/tcell/v2"
)
func main() {
// fd := int(os.Stdout.Fd())
// term.MakeRaw(fd)
s, err := tcell.NewScreen()
term := term.NewTerminal(io.ReadWriter(os.Stdout), ">")
if err != nil {
log.Fatalf("%~v", err)
}
if err := s.Init(); err != nil {
log.Fatalf("%+v", err)
}
// width, height := s.Size()
// if width < 50 || height < 50 {
// log.Fatalf("Your terminal must be at least 50x50")
// }
defStyle := tcell.StyleDefault.Background(tcell.ColorReset).Foreground(tcell.ColorReset)
rect := render.CreateRectangle(
0, 1, 10, 10,
'┌', '─', '┐',
'│', '#', '│',
'└', '─', '┘',
defStyle,
)
text := render.CreateText(1, 2, 8, 8, "Hello World! How are you today?", defStyle)
layers := render.CreateLayeredDrawContainer()
layers.Insert(0, rect)
layers.Insert(1, text)
layers.Remove(text.UniqueId())
events := make(chan tcell.Event)
quit := make(chan struct{})
go s.ChannelEvents(events, quit)
lastTime := time.Now()
for {
deltaTime := 1 + time.Since(lastTime).Microseconds()
lastTime = time.Now()
s.Clear()
fps := 1_000_000 / deltaTime
fpsText := render.CreateText(0, 0, 16, 1, fmt.Sprintf("%v FPS", fps), defStyle)
layers.Draw(s)
fpsText.Draw(s)
s.Show()
select {
case ev, ok := <-events:
if !ok {
break
}
switch ev := ev.(type) {
case *tcell.EventResize:
s.Sync()
case *tcell.EventKey:
if ev.Key() == tcell.KeyEscape || ev.Key() == tcell.KeyCtrlC {
s.Fini()
os.Exit(0)
}
}
default:
}
}
term.Write([]byte("1\n2\n"))
}

66
render/layers.go Normal file
View file

@ -0,0 +1,66 @@
package render
import (
"slices"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
"github.com/tidwall/btree"
)
type layeredDrawContainer struct {
id uuid.UUID
layers *btree.Map[uint8, []Drawable]
}
func CreateLayeredDrawContainer() *layeredDrawContainer {
container := new(layeredDrawContainer)
container.layers = btree.NewMap[uint8, []Drawable](2)
return container
}
func (ldc *layeredDrawContainer) Insert(zLevel uint8, drawable Drawable) {
arr, found := ldc.layers.Get(zLevel)
if !found {
arr = make([]Drawable, 1, 2)
}
arr = append(arr, drawable)
ldc.layers.Set(zLevel, arr)
}
func (ldc *layeredDrawContainer) Remove(id uuid.UUID) {
ldc.layers.ScanMut(func(key uint8, value []Drawable) bool {
newSlices := slices.DeleteFunc(value, func(v Drawable) bool { return v.UniqueId() == id })
ldc.layers.Set(key, newSlices)
if len(newSlices) != len(value) {
return false // the slice has been modified, we have found the drawable. Return false to stop iteration.
} else {
return true // we haven't found it yet, keep going
}
})
}
func (ldc *layeredDrawContainer) Clear() {
ldc.layers = btree.NewMap[uint8, []Drawable](2)
}
func (ldc *layeredDrawContainer) UniqueId() uuid.UUID {
return ldc.id
}
func (ldc *layeredDrawContainer) Draw(s tcell.Screen) {
ldc.layers.Ascend(0, func(key uint8, value []Drawable) bool {
for _, d := range value {
d.Draw(s)
}
return true
})
}

290
render/render.go Normal file
View file

@ -0,0 +1,290 @@
package render
import (
"mvvasilev/last_light/util"
"strings"
"unicode/utf8"
"github.com/gdamore/tcell/v2"
"github.com/google/uuid"
)
type Drawable interface {
UniqueId() uuid.UUID
Draw(s tcell.Screen)
}
type rectangle struct {
id uuid.UUID
size util.Size
position util.Position
style tcell.Style
northBorder rune
westBorder rune
eastBorder rune
southBorder rune
nwCorner rune
swCorner rune
seCorner rune
neCorner rune
fillRune rune
}
func CreateSimpleRectangle(x uint16, y uint16, width uint16, height uint16, borderRune rune, fillRune rune, style tcell.Style) rectangle {
return CreateRectangle(
x, y, width, height,
borderRune, borderRune, borderRune,
borderRune, fillRune, borderRune,
borderRune, borderRune, borderRune,
style,
)
}
// CreateRectangle(
//
// x, y, width, height,
// '┌', '─', '┐',
// '│', ' ', '│',
// '└', '─', '┘',
// style
//
// )
func CreateRectangle(
x uint16,
y uint16,
width uint16,
height uint16,
nwCorner, northBorder, neCorner,
westBorder, fillRune, eastBorder,
swCorner, southBorder, seCorner rune,
style tcell.Style,
) rectangle {
return rectangle{
id: uuid.New(),
size: util.SizeOf(width, height),
position: util.PositionAt(x, y),
style: style,
northBorder: northBorder,
eastBorder: eastBorder,
southBorder: southBorder,
westBorder: westBorder,
nwCorner: nwCorner,
seCorner: seCorner,
swCorner: swCorner,
neCorner: neCorner,
fillRune: fillRune,
}
}
func (rect rectangle) UniqueId() uuid.UUID {
return rect.id
}
func (rect rectangle) Draw(s tcell.Screen) {
width := rect.size.Width()
height := rect.size.Height()
x := rect.position.X()
y := rect.position.Y()
for h := range height {
for w := range width {
// nw corner
if w == 0 && h == 0 {
s.SetContent(x+w, y+h, rect.nwCorner, nil, rect.style)
continue
}
// ne corner
if w == (width-1) && h == 0 {
s.SetContent(x+w, y+h, rect.neCorner, nil, rect.style)
continue
}
// sw corner
if w == 0 && h == (height-1) {
s.SetContent(x+w, y+h, rect.swCorner, nil, rect.style)
continue
}
// se corner
if w == (width-1) && h == (height-1) {
s.SetContent(x+w, y+h, rect.seCorner, nil, rect.style)
continue
}
// north border
if h == 0 && (w != 0 && w != (width-1)) {
s.SetContent(x+w, y+h, rect.northBorder, nil, rect.style)
continue
}
// south border
if h == (height-1) && (w != 0 && w != (width-1)) {
s.SetContent(x+w, y+h, rect.southBorder, nil, rect.style)
continue
}
// west border
if w == 0 && (h != 0 && h != (height-1)) {
s.SetContent(x+w, y+h, rect.westBorder, nil, rect.style)
continue
}
// east border
if w == (width-1) && (h != 0 && h != (height-1)) {
s.SetContent(x+w, y+h, rect.eastBorder, nil, rect.style)
continue
}
s.SetContent(x+w, y+h, rect.fillRune, nil, rect.style)
}
}
}
type text struct {
id uuid.UUID
content []string
position util.Position
size util.Size
style tcell.Style
}
func CreateText(
x, y uint16,
width, height uint16,
content string,
style tcell.Style,
) text {
return text{
id: uuid.New(),
content: strings.Split(content, " "),
style: style,
size: util.SizeOf(width, height),
position: util.PositionAt(x, y),
}
}
func (t text) UniqueId() uuid.UUID {
return t.id
}
func (t text) Draw(s tcell.Screen) {
width := t.size.Width()
height := t.size.Height()
x := t.position.X()
y := t.position.Y()
currentHPos := 0
currentVPos := 0
drawText := func(text string) {
for i, r := range text {
s.SetContent(x+currentHPos+i, y+currentVPos, r, nil, t.style)
}
}
for _, s := range t.content {
runeCount := utf8.RuneCountInString(s)
if currentVPos > height {
break
}
// The current word cannot fit within the remaining space on the line
if runeCount > (width - currentHPos) {
currentVPos += 1 // next line
currentHPos = 0 // reset to start of line
drawText(s + " ")
currentHPos += runeCount + 1
continue
}
// The current word fits exactly within the remaining space on the line
if runeCount == (width - currentHPos) {
drawText(s)
currentVPos += 1 // next line
currentHPos = 0 // reset to start of line
continue
}
// The current word fits within the remaining space, and there's more space left over
drawText(s + " ")
currentHPos += runeCount + 1 // add +1 to account for space after word
}
}
type grid struct {
id uuid.UUID
internalCellSize util.Size
numCellsHorizontal uint16
numCellsVertical uint16
position util.Position
style tcell.Style
northBorder rune
westBorder rune
eastBorder rune
southBorder rune
nwCorner rune
swCorner rune
seCorner rune
neCorner rune
verticalTJunction rune
horizontalTJunction rune
crossJunction rune
fillRune rune
}
func CreateGrid(
x uint16,
y uint16,
cellWidth uint16,
cellHeight uint16,
numCellsHorizontal uint16,
numCellsVertical uint16,
nwCorner, northBorder, neCorner,
westBorder, fillRune, eastBorder,
swCorner, southBorder, seCorner,
verticalTJunction, horizontalTJunction,
crossJunction rune,
style tcell.Style,
) grid {
return grid{
id: uuid.New(),
internalCellSize: util.SizeOf(cellWidth, cellHeight),
numCellsHorizontal: numCellsHorizontal,
numCellsVertical: numCellsVertical,
position: util.PositionAt(x, y),
style: style,
northBorder: northBorder,
eastBorder: eastBorder,
southBorder: southBorder,
westBorder: westBorder,
nwCorner: nwCorner,
seCorner: seCorner,
swCorner: swCorner,
neCorner: neCorner,
fillRune: fillRune,
verticalTJunction: verticalTJunction,
horizontalTJunction: horizontalTJunction,
crossJunction: crossJunction,
}
}
func (g grid) Draw(s tcell.Screen) {
}

1
terminal.go Normal file
View file

@ -0,0 +1 @@
package main

35
util/util.go Normal file
View file

@ -0,0 +1,35 @@
package util
type Position struct {
x int
y int
}
func PositionAt(x uint16, y uint16) Position {
return Position{int(x), int(y)}
}
func (p Position) X() int {
return p.x
}
func (p Position) Y() int {
return p.y
}
type Size struct {
width int
height int
}
func SizeOf(width uint16, height uint16) Size {
return Size{int(width), int(height)}
}
func (s Size) Width() int {
return s.width
}
func (s Size) Height() int {
return s.height
}