From 6d576939db3e606336c4df8da56a93afc8b1a665 Mon Sep 17 00:00:00 2001 From: mvvasilev Date: Fri, 19 Apr 2024 16:56:43 +0300 Subject: [PATCH] Layered container, rectangles, text, async event handling --- .vscode/launch.json | 16 +++ DESIGN.md | 30 +++++ go.mod | 7 +- go.sum | 4 + layer.go | 1 + main.go | 86 ++++++++++++- render/layers.go | 66 ++++++++++ render/render.go | 290 ++++++++++++++++++++++++++++++++++++++++++++ terminal.go | 1 + util/util.go | 35 ++++++ 10 files changed, 529 insertions(+), 7 deletions(-) create mode 100644 .vscode/launch.json create mode 100644 DESIGN.md create mode 100644 layer.go create mode 100644 render/layers.go create mode 100644 render/render.go create mode 100644 terminal.go create mode 100644 util/util.go diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..358acf4 --- /dev/null +++ b/.vscode/launch.json @@ -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" + } + ] +} \ No newline at end of file diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 0000000..dacfc68 --- /dev/null +++ b/DESIGN.md @@ -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 diff --git a/go.mod b/go.mod index c4ab2d3..1849e18 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 748b6a8..70d4a12 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/layer.go b/layer.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/layer.go @@ -0,0 +1 @@ +package main diff --git a/main.go b/main.go index ab70687..0e66e91 100644 --- a/main.go +++ b/main.go @@ -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")) } diff --git a/render/layers.go b/render/layers.go new file mode 100644 index 0000000..1301429 --- /dev/null +++ b/render/layers.go @@ -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 + }) +} diff --git a/render/render.go b/render/render.go new file mode 100644 index 0000000..aaef03c --- /dev/null +++ b/render/render.go @@ -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) { + +} diff --git a/terminal.go b/terminal.go new file mode 100644 index 0000000..06ab7d0 --- /dev/null +++ b/terminal.go @@ -0,0 +1 @@ +package main diff --git a/util/util.go b/util/util.go new file mode 100644 index 0000000..da1ac52 --- /dev/null +++ b/util/util.go @@ -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 +}