mirror of
https://github.com/mvvasilev/last_light.git
synced 2025-04-11 17:25:01 +03:00
Layered container, rectangles, text, async event handling
This commit is contained in:
parent
b4ee5a7f13
commit
6d576939db
10 changed files with 529 additions and 7 deletions
16
.vscode/launch.json
vendored
Normal file
16
.vscode/launch.json
vendored
Normal 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
30
DESIGN.md
Normal 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
7
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
|
||||
|
|
4
go.sum
4
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=
|
||||
|
|
1
layer.go
Normal file
1
layer.go
Normal file
|
@ -0,0 +1 @@
|
|||
package main
|
86
main.go
86
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"))
|
||||
}
|
||||
|
|
66
render/layers.go
Normal file
66
render/layers.go
Normal 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
290
render/render.go
Normal 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
1
terminal.go
Normal file
|
@ -0,0 +1 @@
|
|||
package main
|
35
util/util.go
Normal file
35
util/util.go
Normal 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
|
||||
}
|
Loading…
Add table
Reference in a new issue